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
-
-## 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();
+ }
+ },
+ });
}