diff --git a/logistics-sentry/.env.local b/logistics-sentry/.env.local new file mode 100644 index 000000000..d58e492e2 --- /dev/null +++ b/logistics-sentry/.env.local @@ -0,0 +1 @@ +TINYFISH_API_KEY=sk-mino-t0y8BUE216q0n4T7Jsm7MAXdNNbIGvYv diff --git a/logistics-sentry/Demo Video.mp4 b/logistics-sentry/Demo Video.mp4 deleted file mode 100644 index e4ea00da0..000000000 Binary files a/logistics-sentry/Demo Video.mp4 and /dev/null differ diff --git a/logistics-sentry/README.md b/logistics-sentry/README.md index a195dcfeb..9bfd0c3c5 100644 --- a/logistics-sentry/README.md +++ b/logistics-sentry/README.md @@ -1,39 +1,75 @@ # TinyFish - Logistics Intelligence Sentry -Live Demo: [https://inventory-agent-three.vercel.app/](https://inventory-agent-three.vercel.app/) + +**Live Demo:** [https://inventory-agent-three.vercel.app/](https://inventory-agent-three.vercel.app/) A comprehensive logistics intelligence platform that helps supply chain teams track port congestion, carrier advisories, and operational risks across multiple sources simultaneously. Uses the **Discovery → Scouting → Synthesis** pipeline pattern with parallel TinyFish browser agents to provide real-time, source-backed operational signals. ## Demo -![Demo Video](Demo%20Video.mp4) -## How TinyFish API is Used -The TinyFish API powers the core execution layer. The orchestrator deploys **multiple TinyFish Agents** to navigate the live DOM of target logistics sites, bypassing static API limitations. These agents extract "Deep Metrics" (wait times, vessel counts, specific alerts) and return structured operational signals. +> Add your demo video or screenshot here + +## How TinyFish APIs are Used + +This app uses two TinyFish APIs: + +**Search API** — used in the discovery phase when a port or carrier is not in the knowledge base. Instead of sending a browser agent to DuckDuckGo, the Search API finds the right official URLs directly: + +```javascript +import { TinyFish } from "@tiny-fish/sdk"; + +const client = new TinyFish({ apiKey: process.env.TINYFISH_API_KEY }); + +const res = await client.search.query({ + query: "Port of Rotterdam port authority operations status advisories", +}); + +const urls = res.results.slice(0, 2).map(r => r.url); +``` + +**Agent API** — used in the scouting phase to navigate discovered URLs, extract deep metrics, quotes, and operational signals: + +```javascript +const stream = await client.agent.stream( + { url, goal, browser_profile: "stealth" } +); + +for await (const event of stream) { + if (event.type === EventType.COMPLETE) { + // event.result contains structured signals JSON + break; + } +} +``` ## Intelligence Lifecycle -The following sequence diagram illustrates the end-to-end flow of a risk assessment, from discovery to synthesis. ```mermaid sequenceDiagram participant User participant API as Orchestrator (API) participant KB as Knowledge Base - participant TinyFish as TinyFish Agents + participant Search as TinyFish Search API + participant Agent as TinyFish Agent API participant Web as Live Logistics Web participant Logic as Risk Engine User->>API: POST /risk-assessment (Context) API->>KB: Resolve Target URLs - Note right of API: Discovery mode triggered if no matches - - API->>TinyFish: Spawn Parallel Swarm (URL + Mission) - + + alt Port/Carrier not in Knowledge Base + API->>Search: Search for official URLs + Search-->>API: Ranked results with URLs + end + + API->>Agent: Spawn Parallel Swarm (URL + Mission) + par Agent Orchestration - TinyFish->>Web: Navigate & Analyze DOM - Web-->>TinyFish: HTML Content - TinyFish->>TinyFish: Extract Metrics & Quotes + Agent->>Web: Navigate & Analyze DOM + Web-->>Agent: HTML Content + Agent->>Agent: Extract Metrics & Quotes end - - TinyFish-->>API: Return Structured Signals (JSON) + + Agent-->>API: Return Structured Signals (JSON) API->>Logic: Synthesize Findings Logic->>Logic: Apply Decision Matrix Logic-->>API: Consolidated Risk Profile @@ -41,82 +77,41 @@ sequenceDiagram ``` ## Risk Decision Logic -The system normalizes unstructured signals into a coherent risk level based on the following state logic. ```mermaid stateDiagram-v2 [*] --> Scanning - + Scanning --> Normal: No Negative Signals Found Scanning --> SignalsDetected: Metrics/Quotes Extracted - + SignalsDetected --> LowRisk: Minor Congestion (wait < 2 days) SignalsDetected --> MediumRisk: Moderate Congestion (wait 2-4 days) SignalsDetected --> HighRisk: Strike / Force Majeure / Severe Wait (> 4 days) - + LowRisk --> Monitoring: "Continue monitoring" MediumRisk --> AlertSubscriber: "Anticipate berthing delay" HighRisk --> CrisisAction: "Divert cargo immediately" - + Normal --> [*] Monitoring --> [*] AlertSubscriber --> [*] CrisisAction --> [*] ``` -## Code Snippet -```javascript -// Example: Requesting a Risk Assessment in the Logistics Sentry -const response = await fetch("/api/logistics/risk-assessment", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - origin_port: "Port of Los Angeles", - carrier: "Maersk", - mode: "Sea Freight" - }), -}); - -const data = await response.json(); -// Returns a structured Risk Profile with confidence scores and root causes -``` - ## How to Run + ### Prerequisites + - Node.js 18+ -- TinyFish API key (get from [tinyfish.ai](https://tinyfish.ai)) +- TinyFish API key ([get one here](https://agent.tinyfish.ai/api-keys)) ### Setup -1. **Install dependencies**: - ```bash - npm install - ``` -2. **Configure Environment**: - Create a `.env.local` file with: - ```bash - TINYFISH_API_KEY=xxx - ``` -3. **Run development server**: - ```bash - npm run dev - ``` - -## Architecture Diagram -```mermaid -graph TD - UI[USER INTERFACE
Next.js 14 + Framer Motion] - API[Risk Orchestrator
api/logistics/risk-assessment] - - TinyFish[TINYFISH BROWSER AGENTS
Execution Layer] - Web[LIVE PUBLIC WEB
Ports / Carriers / Alerts] - Logic[RISK ENGINE
Synthesis & Decisioning] - - UI -->|Route Context| API - API -->|1. Discovery| API - API -->|2. Parallel Swarm| TinyFish - TinyFish -->|3. Scrape & Reason| Web - Web -->|Unstructured Data| TinyFish - TinyFish -->|Structured Signals| Logic - Logic -->|JSON Risk Profile| API - API -->|4. Assessment| UI + +1. Install dependencies: + +```bash +npm install ``` + +2. Create a `.env.local` file: diff --git a/logistics-sentry/env.example b/logistics-sentry/env.example new file mode 100644 index 000000000..8000822da --- /dev/null +++ b/logistics-sentry/env.example @@ -0,0 +1 @@ +TINYFISH_API_KEY=your_tinyfish_api_key_here diff --git a/logistics-sentry/package-lock.json b/logistics-sentry/package-lock.json index 7ffedf62b..435d67594 100644 --- a/logistics-sentry/package-lock.json +++ b/logistics-sentry/package-lock.json @@ -8,6 +8,7 @@ "name": "inventory-risk-agent", "version": "0.1.0", "dependencies": { + "@tiny-fish/sdk": "latest", "clsx": "^2.1.1", "framer-motion": "^11.18.2", "lucide-react": "^0.378.0", @@ -521,6 +522,18 @@ "tslib": "^2.4.0" } }, + "node_modules/@tiny-fish/sdk": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@tiny-fish/sdk/-/sdk-0.0.7.tgz", + "integrity": "sha512-VzBTKwYfJZEkNpj56kyBGnT6m8DNaXdDSaDImTC6t/9IyIcXguqrB83pyioRs23vIhQs+soNs8vgYASALDmtsA==", + "dependencies": { + "p-retry": "^7.1.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3413,6 +3426,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4288,6 +4313,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6084,6 +6124,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/logistics-sentry/package.json b/logistics-sentry/package.json index 1d78fd3c9..dc0a5c0a0 100644 --- a/logistics-sentry/package.json +++ b/logistics-sentry/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@tiny-fish/sdk": "latest", "clsx": "^2.1.1", "framer-motion": "^11.18.2", "lucide-react": "^0.378.0", diff --git a/logistics-sentry/src/lib/logistics/agent.js b/logistics-sentry/src/lib/logistics/agent.js index d5e8039ac..85d6b4f92 100644 --- a/logistics-sentry/src/lib/logistics/agent.js +++ b/logistics-sentry/src/lib/logistics/agent.js @@ -1,4 +1,5 @@ import { runGenericAgent } from "../tinyfish"; +import { TinyFish } from "@tiny-fish/sdk"; // --- STAGE 1: SOURCE DISCOVERY (KNOWLEDGE BASE) --- // In a production system, this would be an LLM or specific search agent. @@ -58,74 +59,46 @@ const SOURCE_KNOWLEDGE_BASE = { // Contextual sources like Weather or Labor generic sites could be added }; -function buildDiscoverySources(origin_port, carrier) { +async function buildDiscoverySources(origin_port, carrier) { + const client = new TinyFish({ apiKey: process.env.TINYFISH_API_KEY }); const sources = []; - if (origin_port) { - const portQuery = encodeURIComponent(`${origin_port} port authority operations status`); - sources.push({ - name: `Discovery: ${origin_port} Port Authority`, - url: `https://duckduckgo.com/html/?q=${portQuery}`, - type: "custom_discovery", - goal: ` -### MISSION: PORT AUTHORITY INTELLIGENCE DISCOVERY -TARGET: ${origin_port} - -You are a Logistics Intelligence Scout. Your job is to locate the official port authority or terminal operations page for ${origin_port}, then extract operational status signals. - -### INSTRUCTIONS: -1. Search for the official port authority or terminal operations page for ${origin_port}. -2. Navigate to the official source and look for operational updates, advisories, or congestion metrics. -3. Extract specific metrics/quotes with dates where possible. -### REQUIRED OUTPUT (JSON ONLY): -{ - "scan_status": "completed", - "operational_status": "NORMAL" | "DISRUPTED" | "UNKNOWN", - "signals": [ - { - "summary": "Detailed finding with numbers/quotes if available", - "severity": "LOW" | "MEDIUM" | "HIGH", - "date": "YYYY-MM-DD", - "category": "METRIC" | "QUOTE" | "STATUS" - } - ] -} -` - }); + if (origin_port) { + try { + const res = await client.search.query({ + query: `${origin_port} port authority operations status advisories`, + }); + const top = res.results?.slice(0, 2) || []; + top.forEach((r) => { + sources.push({ + name: r.title || `Discovery: ${origin_port}`, + url: r.url, + type: "port_authority", + }); + }); + } catch (e) { + console.error(`[Search] Port discovery failed for ${origin_port}:`, e.message); + } } - if (carrier) { - const carrierQuery = encodeURIComponent(`${carrier} customer advisories`); - sources.push({ - name: `Discovery: ${carrier} Advisories`, - url: `https://duckduckgo.com/html/?q=${carrierQuery}`, - type: "custom_discovery", - goal: ` -### MISSION: CARRIER ADVISORY INTELLIGENCE DISCOVERY -TARGET: ${carrier} -You are a Logistics Intelligence Scout. Your job is to locate ${carrier}'s official customer advisories or operations updates page, then extract operational signals. - -### INSTRUCTIONS: -1. Find the official ${carrier} advisories/alerts/newsroom page. -2. Navigate to the most recent advisories and extract concrete metrics or dates. -3. Prefer official carrier sources over third-party news. - -### REQUIRED OUTPUT (JSON ONLY): -{ - "scan_status": "completed", - "operational_status": "NORMAL" | "DISRUPTED" | "UNKNOWN", - "signals": [ - { - "summary": "Detailed finding with numbers/quotes if available", - "severity": "LOW" | "MEDIUM" | "HIGH", - "date": "YYYY-MM-DD", - "category": "METRIC" | "QUOTE" | "STATUS" - } - ] -} -` - }); + if (carrier) { + try { + const res = await client.search.query({ + query: `${carrier} shipping customer advisories operational updates`, + }); + const top = res.results?.slice(0, 2) || []; + top.forEach((r) => { + sources.push({ + name: r.title || `Discovery: ${carrier}`, + url: r.url, + type: "carrier_advisory", + }); + }); + } catch (e) { + console.error(`[Search] Carrier discovery failed for ${carrier}:`, e.message); + } } + return sources; } @@ -447,7 +420,7 @@ export async function assessDelayRisk(context) { let discoveryUsed = false; if (sources.length === 0) { - sources = buildDiscoverySources(origin_port, carrier); + sources = await buildDiscoverySources(origin_port, carrier); discoveryUsed = sources.length > 0; if (!discoveryUsed) { return { diff --git a/logistics-sentry/src/lib/pricing-intelligence.js b/logistics-sentry/src/lib/pricing-intelligence.js index a3bc46f8c..34de3c127 100644 --- a/logistics-sentry/src/lib/pricing-intelligence.js +++ b/logistics-sentry/src/lib/pricing-intelligence.js @@ -1,13 +1,14 @@ "use server"; -const TINYFISH_API_URL = "https://tinyfish.ai/v1/automation/run-sse"; -const TINYFISH_API_KEY = process.env.TINYFISH_API_KEY; +import { TinyFish, EventType, RunStatus } from "@tiny-fish/sdk"; + +const client = new TinyFish({ apiKey: process.env.TINYFISH_API_KEY }); export async function runPricingAnalysis(competitorUrl, options = {}) { - if (!TINYFISH_API_KEY) throw new Error("Missing TINYFISH_API_KEY environment variable"); - if (!competitorUrl) throw new Error("Missing competitorUrl"); + if (!process.env.TINYFISH_API_KEY) throw new Error("Missing TINYFISH_API_KEY environment variable"); + if (!competitorUrl) throw new Error("Missing competitorUrl"); - const goal = ` + const goal = ` ### MISSION: COMPETITIVE PRICING INTELLIGENCE (Target: ${competitorUrl}) You are a senior Strategic Pricing Analyst. Your mission is to extract the exact pricing and packaging model for the specified competitor. @@ -61,28 +62,39 @@ You are a senior Strategic Pricing Analyst. Your mission is to extract the exact } `; - try { - const response = await fetch(TINYFISH_API_URL, { - method: "POST", - headers: { - "X-API-Key": TINYFISH_API_KEY, - "Content-Type": "application/json", - }, - signal: options.signal, - body: JSON.stringify({ - url: competitorUrl, - goal: goal, - browser_profile: "stealth" - }), - }); + const encoder = new TextEncoder(); - if (!response.ok) { - throw new Error(`TinyFish API error for ${competitorUrl}: ${response.statusText}`); - } + // Return a ReadableStream compatible with the existing pricing/run/route.js + // SSE parser — it reads raw "data: ..." lines and looks for final_result. + return new ReadableStream({ + async start(controller) { + const sendEvent = (data) => { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); + }; - return response.body; - } catch (error) { - console.error(`Agent execution failed for ${competitorUrl}:`, error); - throw error; - } + try { + const stream = await client.agent.stream( + { url: competitorUrl, goal, browser_profile: "stealth" }, + { signal: options.signal } + ); + + for await (const event of stream) { + sendEvent(event); + + if (event.type === EventType.COMPLETE) { + if (event.status === RunStatus.COMPLETED) { + // TypeScript SDK: event.result (not event.result_json) + sendEvent({ final_result: event.result ?? null }); + } + break; // always break after COMPLETE + } + } + } catch (error) { + console.error(`Agent execution failed for ${competitorUrl}:`, error); + sendEvent({ type: "error", message: error.message }); + } finally { + controller.close(); + } + }, + }); } diff --git a/logistics-sentry/src/lib/tinyfish.js b/logistics-sentry/src/lib/tinyfish.js index e6c3cda7e..e07ea92af 100644 --- a/logistics-sentry/src/lib/tinyfish.js +++ b/logistics-sentry/src/lib/tinyfish.js @@ -1,17 +1,20 @@ "use server"; -const TINYFISH_API_URL = "https://tinyfish.ai/v1/automation/run-sse"; -const TINYFISH_API_KEY = process.env.TINYFISH_API_KEY; +import { TinyFish, EventType, RunStatus } from "@tiny-fish/sdk"; + +const client = new TinyFish({ apiKey: process.env.TINYFISH_API_KEY }); export async function runAgent(sku, intendedUpdate, contextUrl, options = {}) { const targetUrl = contextUrl || "https://inventory-demo-dashboard.com"; - const missionType = intendedUpdate ? `cross-verify the safety of an intended stock update ("${intendedUpdate}")` : `perform a general integrity audit to ensure data consistency across sources`; + const missionType = intendedUpdate + ? `cross-verify the safety of an intended stock update ("${intendedUpdate}")` + : `perform a general integrity audit to ensure data consistency across sources`; const goal = ` ### MISSION: DEEP INTEGRITY AUDIT (SKU: ${sku}) You are a senior Autonomous Inventory Auditor. Your mission is to ${missionType} by investigating multiple data sources. -${contextUrl ? `\n**CRITICAL CONTEXT**: The user has provided a specific Source of Truth URL: ${contextUrl}. You MUST navigate to this URL to verify the "Actual Stock" against the dashboard's "Reported Stock".\n` : ''} +${contextUrl ? `\n**CRITICAL CONTEXT**: The user has provided a specific Source of Truth URL: ${contextUrl}. You MUST navigate to this URL to verify the "Actual Stock" against the dashboard's "Reported Stock".\n` : ""} ### AUDIT PHASES 1. **PHASE_1: SURFACE_SCAN (Dashboard)** @@ -20,12 +23,16 @@ ${contextUrl ? `\n**CRITICAL CONTEXT**: The user has provided a specific Source - Report: {"phase": "SURFACE_SCAN", "status": "completed", "findings": "Extracted reported stock"} 2. **PHASE_2: SOURCE_VERIFICATION (Audit Logs / External Source)** - ${contextUrl ? `- **Navigate to the provided Source URL**: ${contextUrl}\n - Search for ${sku} in the external sheet/feed.\n - Compare the "Stock" value there with the Dashboard value.` : `- Find and navigate to the "Audit Logs" or "History" section.\n - Search for ${sku} and check the last 5 manual entries.\n - Identify if the "User ID" or "Source" of recent changes looks suspicious or anomalous.`} + ${contextUrl + ? `- **Navigate to the provided Source URL**: ${contextUrl}\n - Search for ${sku} in the external sheet/feed.\n - Compare the "Stock" value there with the Dashboard value.` + : `- Find and navigate to the "Audit Logs" or "History" section.\n - Search for ${sku} and check the last 5 manual entries.\n - Identify if the "User ID" or "Source" of recent changes looks suspicious or anomalous.`} - Report: {"phase": "SOURCE_VERIFICATION", "status": "completed", "findings": "Verified log/source integrity"} 3. **PHASE_3: BUSINESS_CONTEXT (Sales Analytics)** - Navigate to "Sales Analytics" or "Orders" view. - ${intendedUpdate ? `- Determine if the "Sales Velocity" for ${sku} justifies the intended update: "${intendedUpdate}".\n - Check for pending shipments that might conflict with this update.` : `- Analyze "Sales Velocity" for ${sku} to detect any anomalies vs reported stock.\n - Identify if current stock levels are dangerously low or high based on sales trends.`} + ${intendedUpdate + ? `- Determine if the "Sales Velocity" for ${sku} justifies the intended update: "${intendedUpdate}".\n - Check for pending shipments that might conflict with this update.` + : `- Analyze "Sales Velocity" for ${sku} to detect any anomalies vs reported stock.\n - Identify if current stock levels are dangerously low or high based on sales trends.`} - Report: {"phase": "BUSINESS_CONTEXT", "status": "completed", "findings": "Analyzed sales alignment"} 4. **PHASE_4: SYNTHESIS & VERDICT** @@ -50,56 +57,76 @@ ${contextUrl ? `\n**CRITICAL CONTEXT**: The user has provided a specific Source } `; - try { - const response = await fetch(TINYFISH_API_URL, { - method: "POST", - headers: { - "X-API-Key": TINYFISH_API_KEY, - "Content-Type": "application/json", - }, - signal: options.signal, - body: JSON.stringify({ - url: targetUrl, - goal: goal, - browser_profile: "stealth" - }), - }); - - if (!response.ok) { - throw new Error(`TinyFish API error: ${response.statusText}`); - } - - // Since this is SSE, we return the stream or a reader - return response.body; - } catch (error) { - console.error("Agent execution failed:", error); - throw error; - } + // Return a ReadableStream that mirrors SDK SSE events so the existing + // API route can pass it through unchanged to the browser. + const encoder = new TextEncoder(); + + return new ReadableStream({ + async start(controller) { + const sendEvent = (data) => { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); + }; + + try { + const stream = await client.agent.stream( + { url: targetUrl, goal, browser_profile: "stealth" }, + { signal: options.signal } + ); + + for await (const event of stream) { + sendEvent(event); + + if (event.type === EventType.COMPLETE) { + // COMPLETED only means the browser ran without crashing — + // always validate result content, not just the status. + if (event.status === RunStatus.COMPLETED) { + // TypeScript SDK: event.result (not event.result_json) + sendEvent({ final_result: event.result ?? null }); + } + break; // always break after COMPLETE + } + } + } catch (error) { + console.error("Agent execution failed:", error); + sendEvent({ type: "error", message: error.message }); + } finally { + controller.close(); + } + }, + }); } export async function runGenericAgent(url, goal, options = {}) { - try { - const response = await fetch(TINYFISH_API_URL, { - method: "POST", - headers: { - "X-API-Key": TINYFISH_API_KEY, - "Content-Type": "application/json", - }, - signal: options.signal, - body: JSON.stringify({ - url: url, - goal: goal, - browser_profile: "stealth" - }), - }); - - if (!response.ok) { - throw new Error(`TinyFish API error: ${response.statusText}`); - } - - return response.body; - } catch (error) { - console.error("Agent execution failed:", error); - throw error; - } + const encoder = new TextEncoder(); + + return new ReadableStream({ + async start(controller) { + const sendEvent = (data) => { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); + }; + + try { + const stream = await client.agent.stream( + { url, goal, browser_profile: "stealth" }, + { signal: options.signal } + ); + + for await (const event of stream) { + sendEvent(event); + + if (event.type === EventType.COMPLETE) { + if (event.status === RunStatus.COMPLETED) { + sendEvent({ final_result: event.result ?? null }); + } + break; // always break after COMPLETE + } + } + } catch (error) { + console.error("Agent execution failed:", error); + sendEvent({ type: "error", message: error.message }); + } finally { + controller.close(); + } + }, + }); }