diff --git a/agrifine-extension/src/ag-refine/agent.js b/agrifine-extension/src/ag-refine/agent.js
new file mode 100644
index 0000000..75af4ba
--- /dev/null
+++ b/agrifine-extension/src/ag-refine/agent.js
@@ -0,0 +1,127 @@
+import { sessionGet, KEYS, buildContextBundle } from '../utils/storage.js';
+import { TOOL_DEFINITIONS, executeTool } from './tools.js';
+
+const MODEL = 'claude-sonnet-4-6';
+const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
+const MAX_ITERATIONS = 10;
+
+/**
+ * AgrifineAgent β agentic loop with tool use.
+ *
+ * Runs entirely in the sidebar context via the background worker proxy.
+ * Each call to run() streams back events via an onEvent callback so the
+ * UI can update incrementally as the agent thinks and calls tools.
+ */
+export class AgrifineAgent {
+ constructor({ onEvent }) {
+ this.onEvent = onEvent; // ({ type, data }) => void
+ }
+
+ async run(userMessage) {
+ const apiKey = await sessionGet(KEYS.API_KEY);
+ if (!apiKey) {
+ this.onEvent({ type: 'error', data: 'No API key set. Open β Settings to add your Anthropic key.' });
+ return;
+ }
+
+ const contextBundle = await buildContextBundle();
+
+ const systemPrompt = [
+ 'You are AgriAgent, an expert AI assistant for farm operations management.',
+ 'You have access to the user\'s farm data through tools β always use them before answering.',
+ 'When answering questions about fields, weather, yields, or finances: first query the relevant data, then synthesize a clear answer.',
+ 'Be specific: cite field names, dates, acreage, and numbers from the actual data.',
+ 'For weather queries on a field, always look up the field profile first to get coordinates.',
+ '',
+ 'FARM CONTEXT (reading list summaries + field profiles):',
+ contextBundle,
+ ].join('\n');
+
+ const messages = [{ role: 'user', content: userMessage }];
+
+ this.onEvent({ type: 'thinking', data: 'Analysing your questionβ¦' });
+
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
+ const body = {
+ model: MODEL,
+ max_tokens: 2048,
+ system: systemPrompt,
+ tools: TOOL_DEFINITIONS,
+ messages,
+ };
+
+ let response;
+ try {
+ response = await this._callAPI(apiKey, body);
+ } catch (err) {
+ this.onEvent({ type: 'error', data: err.message });
+ return;
+ }
+
+ // Append assistant turn
+ messages.push({ role: 'assistant', content: response.content });
+
+ if (response.stop_reason === 'end_turn') {
+ // Extract final text
+ const text = response.content
+ .filter((b) => b.type === 'text')
+ .map((b) => b.text)
+ .join('\n');
+ this.onEvent({ type: 'answer', data: text });
+ return;
+ }
+
+ if (response.stop_reason === 'tool_use') {
+ const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
+ const toolResults = [];
+
+ for (const block of toolUseBlocks) {
+ this.onEvent({ type: 'tool_call', data: { name: block.name, input: block.input } });
+
+ let result;
+ try {
+ result = await executeTool(block.name, block.input);
+ } catch (err) {
+ result = { error: err.message };
+ }
+
+ this.onEvent({ type: 'tool_result', data: { name: block.name, result } });
+
+ toolResults.push({
+ type: 'tool_result',
+ tool_use_id: block.id,
+ content: JSON.stringify(result),
+ });
+ }
+
+ messages.push({ role: 'user', content: toolResults });
+ continue;
+ }
+
+ // Unexpected stop reason
+ this.onEvent({ type: 'error', data: `Unexpected stop reason: ${response.stop_reason}` });
+ return;
+ }
+
+ this.onEvent({ type: 'error', data: 'Agent reached maximum iterations without completing.' });
+ }
+
+ async _callAPI(apiKey, body) {
+ const res = await fetch(ANTHROPIC_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey,
+ 'anthropic-version': '2023-06-01',
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Anthropic API ${res.status}: ${text}`);
+ }
+
+ return res.json();
+ }
+}
diff --git a/agrifine-extension/src/ag-refine/index.js b/agrifine-extension/src/ag-refine/index.js
new file mode 100644
index 0000000..d870629
--- /dev/null
+++ b/agrifine-extension/src/ag-refine/index.js
@@ -0,0 +1,234 @@
+import { AgrifineAgent } from './agent.js';
+
+const TOOL_ICONS = {
+ get_reading_list: 'π',
+ get_field_profiles: 'π±',
+ get_ingested_files: 'π',
+ get_weather: 'π€οΈ',
+ lookup_usda_soil: 'ποΈ',
+ calculate_gdd: 'π',
+};
+
+const SUGGESTED_PROMPTS = [
+ 'What are my current field conditions and harvest windows?',
+ 'Which fields have the best soil for carbon sequestration?',
+ 'Summarise all my farm data and flag any issues',
+ 'What does the 7-day weather look like for my fields?',
+ 'What USDA programs might I qualify for based on my fields?',
+];
+
+export function AgRefineModule() {
+ let messages = [];
+ let isRunning = false;
+
+ return {
+ id: 'ag-refine',
+ label: 'AgriAgent',
+
+ async render(container) {
+ container.innerHTML = `
+
+
+
+
+
+ π€
+
AgriAgent
+ AI Agent
+
+
Multi-step reasoning over all your farm data
+
+
+
+
+
+
+
+
Try askingβ¦
+
+ ${SUGGESTED_PROMPTS.map((p) => `
+ `).join('')}
+
+
+
+
+
+
+ `;
+
+ this._bindEvents(container);
+ this._renderMessages(container);
+ },
+
+ _bindEvents(container) {
+ const input = container.querySelector('#agent-input');
+ const sendBtn = container.querySelector('#agent-send');
+
+ const send = () => {
+ const text = input.value.trim();
+ if (!text || isRunning) return;
+ input.value = '';
+ this._runAgent(text, container);
+ };
+
+ sendBtn.addEventListener('click', send);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
+ });
+
+ container.querySelectorAll('.suggest-btn').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ input.value = btn.textContent.trim();
+ send();
+ });
+ });
+
+ container.querySelector('#agent-clear').addEventListener('click', () => {
+ messages = [];
+ isRunning = false;
+ this._renderMessages(container);
+ });
+ },
+
+ async _runAgent(userText, container) {
+ if (isRunning) return;
+ isRunning = true;
+
+ // Hide suggestions
+ container.querySelector('#agent-suggestions')?.classList.add('hidden');
+
+ // Add user message
+ messages.push({ role: 'user', text: userText });
+ this._renderMessages(container);
+
+ // Thinking placeholder
+ const thinkingId = `thinking_${Date.now()}`;
+ messages.push({ role: 'thinking', id: thinkingId, steps: [] });
+ this._renderMessages(container);
+
+ const thinkingMsg = messages[messages.length - 1];
+
+ const agent = new AgrifineAgent({
+ onEvent: ({ type, data }) => {
+ if (type === 'thinking') {
+ thinkingMsg.steps.push({ type: 'status', text: data });
+ } else if (type === 'tool_call') {
+ thinkingMsg.steps.push({
+ type: 'tool',
+ icon: TOOL_ICONS[data.name] ?? 'π§',
+ name: data.name.replace(/_/g, ' '),
+ input: JSON.stringify(data.input),
+ });
+ } else if (type === 'tool_result') {
+ const last = thinkingMsg.steps[thinkingMsg.steps.length - 1];
+ if (last?.type === 'tool') last.done = true;
+ } else if (type === 'answer') {
+ // Replace thinking bubble with final answer
+ const idx = messages.findIndex((m) => m.id === thinkingId);
+ if (idx >= 0) messages.splice(idx, 1);
+ messages.push({ role: 'assistant', text: data });
+ isRunning = false;
+ } else if (type === 'error') {
+ const idx = messages.findIndex((m) => m.id === thinkingId);
+ if (idx >= 0) messages.splice(idx, 1);
+ messages.push({ role: 'error', text: data });
+ isRunning = false;
+ }
+ this._renderMessages(container);
+ },
+ });
+
+ await agent.run(userText);
+ },
+
+ _renderMessages(container) {
+ const chat = container.querySelector('#agent-chat');
+ if (!chat) return;
+
+ if (messages.length === 0) {
+ chat.innerHTML = '';
+ container.querySelector('#agent-suggestions')?.classList.remove('hidden');
+ return;
+ }
+
+ chat.innerHTML = messages.map((msg) => {
+ if (msg.role === 'user') {
+ return `
+
+
+ ${escapeHtml(msg.text)}
+
+
`;
+ }
+
+ if (msg.role === 'thinking') {
+ const steps = msg.steps ?? [];
+ return `
+
+ ${steps.map((step) => {
+ if (step.type === 'status') {
+ return `
+ ${escapeHtml(step.text)}
+
`;
+ }
+ if (step.type === 'tool') {
+ return `
+ ${step.icon}
+ ${step.name}
+ ${step.done ? 'β' : ''}
+
`;
+ }
+ return '';
+ }).join('')}
+ ${steps.length === 0 ? '
Startingβ¦
' : ''}
+
`;
+ }
+
+ if (msg.role === 'assistant') {
+ return `
+
+
π€
+
+ ${escapeHtml(msg.text)}
+
+
`;
+ }
+
+ if (msg.role === 'error') {
+ return `
+
+ β οΈ ${escapeHtml(msg.text)}
+
`;
+ }
+
+ return '';
+ }).join('');
+
+ // Scroll to bottom
+ chat.scrollTop = chat.scrollHeight;
+ },
+ };
+}
+
+function escapeHtml(str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
diff --git a/agrifine-extension/src/ag-refine/tools.js b/agrifine-extension/src/ag-refine/tools.js
new file mode 100644
index 0000000..6d693cd
--- /dev/null
+++ b/agrifine-extension/src/ag-refine/tools.js
@@ -0,0 +1,304 @@
+import { getReadingList, getIngestedFiles, getFieldProfiles } from '../utils/storage.js';
+
+// ββ Tool definitions sent to Claude ββββββββββββββββββββββββββββββββββββββββββ
+
+export const TOOL_DEFINITIONS = [
+ {
+ name: 'get_reading_list',
+ description: 'Retrieve saved web pages from the user\'s reading list. Can filter by tag.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ tag: {
+ type: 'string',
+ description: 'Optional tag to filter by: agriculture, equipment, land, carbon, USDA, dairy, finance, weather',
+ },
+ },
+ required: [],
+ },
+ },
+ {
+ name: 'get_field_profiles',
+ description: 'Retrieve all farm field profiles including acreage, soil type, coordinates, crop history, and notes.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ field_name: {
+ type: 'string',
+ description: 'Optional field name to filter by (partial match)',
+ },
+ },
+ required: [],
+ },
+ },
+ {
+ name: 'get_ingested_files',
+ description: 'Retrieve all uploaded and parsed farm data files (CSV, Excel, PDF). Returns structured JSON extracted from each file.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ type: {
+ type: 'string',
+ enum: ['CSV', 'Excel', 'PDF'],
+ description: 'Optional file type filter',
+ },
+ },
+ required: [],
+ },
+ },
+ {
+ name: 'get_weather',
+ description: 'Fetch current conditions, 7-day forecast, and calculate Growing Degree Days (GDD) for a field location using Open-Meteo (free, no key required).',
+ input_schema: {
+ type: 'object',
+ properties: {
+ latitude: {
+ type: 'number',
+ description: 'Latitude of the field',
+ },
+ longitude: {
+ type: 'number',
+ description: 'Longitude of the field',
+ },
+ field_name: {
+ type: 'string',
+ description: 'Human-readable field name for context',
+ },
+ },
+ required: ['latitude', 'longitude'],
+ },
+ },
+ {
+ name: 'lookup_usda_soil',
+ description: 'Look up soil data from the USDA Web Soil Survey API by coordinates. Returns soil series, texture, organic matter, and drainage class.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ latitude: {
+ type: 'number',
+ description: 'Latitude',
+ },
+ longitude: {
+ type: 'number',
+ description: 'Longitude',
+ },
+ },
+ required: ['latitude', 'longitude'],
+ },
+ },
+ {
+ name: 'calculate_gdd',
+ description: 'Calculate Growing Degree Days from temperature data. Uses base temp of 50Β°F for forage crops.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ daily_highs: {
+ type: 'array',
+ items: { type: 'number' },
+ description: 'Array of daily high temperatures in Fahrenheit',
+ },
+ daily_lows: {
+ type: 'array',
+ items: { type: 'number' },
+ description: 'Array of daily low temperatures in Fahrenheit',
+ },
+ base_temp: {
+ type: 'number',
+ description: 'Base temperature in Β°F (default 50 for forage crops)',
+ },
+ },
+ required: ['daily_highs', 'daily_lows'],
+ },
+ },
+];
+
+// ββ Tool implementations ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export async function executeTool(name, input) {
+ switch (name) {
+ case 'get_reading_list':
+ return toolGetReadingList(input);
+ case 'get_field_profiles':
+ return toolGetFieldProfiles(input);
+ case 'get_ingested_files':
+ return toolGetIngestedFiles(input);
+ case 'get_weather':
+ return toolGetWeather(input);
+ case 'lookup_usda_soil':
+ return toolLookupUSDAsoil(input);
+ case 'calculate_gdd':
+ return toolCalculateGDD(input);
+ default:
+ return { error: `Unknown tool: ${name}` };
+ }
+}
+
+async function toolGetReadingList({ tag } = {}) {
+ const list = await getReadingList();
+ const filtered = tag ? list.filter((i) => i.tags?.includes(tag)) : list;
+ return {
+ count: filtered.length,
+ items: filtered.slice(0, 30).map((i) => ({
+ title: i.title,
+ url: i.url,
+ summary: i.summary,
+ tags: i.tags,
+ savedAt: i.savedAt,
+ })),
+ };
+}
+
+async function toolGetFieldProfiles({ field_name } = {}) {
+ const profiles = await getFieldProfiles();
+ const filtered = field_name
+ ? profiles.filter((p) => p.name.toLowerCase().includes(field_name.toLowerCase()))
+ : profiles;
+ return {
+ count: filtered.length,
+ profiles: filtered.map((p) => ({
+ id: p.id,
+ name: p.name,
+ acres: p.acres,
+ soilType: p.soilType,
+ cluId: p.cluId,
+ coordinates: p.coordinates,
+ notes: p.notes,
+ cropHistory: p.cropHistory,
+ harvestRecords: p.harvestRecords,
+ createdAt: p.createdAt,
+ })),
+ };
+}
+
+async function toolGetIngestedFiles({ type } = {}) {
+ const files = await getIngestedFiles();
+ const filtered = type ? files.filter((f) => f.type === type) : files;
+ return {
+ count: filtered.length,
+ files: filtered.map((f) => ({
+ filename: f.filename,
+ type: f.type,
+ uploadedAt: f.uploadedAt,
+ structuredData: f.structuredData,
+ })),
+ };
+}
+
+async function toolGetWeather({ latitude, longitude, field_name = 'field' }) {
+ const url = new URL('https://api.open-meteo.com/v1/forecast');
+ url.searchParams.set('latitude', latitude);
+ url.searchParams.set('longitude', longitude);
+ url.searchParams.set('current', 'temperature_2m,precipitation,wind_speed_10m,weather_code');
+ url.searchParams.set('daily', 'temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max');
+ url.searchParams.set('temperature_unit', 'fahrenheit');
+ url.searchParams.set('wind_speed_unit', 'mph');
+ url.searchParams.set('precipitation_unit', 'inch');
+ url.searchParams.set('forecast_days', '7');
+ url.searchParams.set('timezone', 'auto');
+
+ const res = await fetch(url.toString());
+ if (!res.ok) throw new Error(`Open-Meteo error: ${res.status}`);
+ const data = await res.json();
+
+ const current = data.current;
+ const daily = data.daily;
+
+ // GDD accumulation from forecast
+ const gddDays = daily.temperature_2m_max.map((hi, i) => {
+ const lo = daily.temperature_2m_min[i];
+ return Math.max(0, ((hi + lo) / 2) - 50);
+ });
+ const totalGDD = gddDays.reduce((a, b) => a + b, 0);
+
+ // Rain alert: >0.5 inch in next 48h
+ const rainAlert = (daily.precipitation_sum[0] ?? 0) + (daily.precipitation_sum[1] ?? 0) > 0.5;
+
+ // Harvest window
+ const avgRainProb = (daily.precipitation_probability_max.slice(0, 3).reduce((a, b) => a + b, 0) / 3);
+ const harvestWindow = avgRainProb < 20 ? 'GREEN' : avgRainProb < 50 ? 'YELLOW' : 'RED';
+
+ return {
+ field: field_name,
+ coordinates: { latitude, longitude },
+ current: {
+ temperature_f: current.temperature_2m,
+ precipitation_in: current.precipitation,
+ wind_mph: current.wind_speed_10m,
+ },
+ forecast_7day: daily.time.map((date, i) => ({
+ date,
+ high_f: daily.temperature_2m_max[i],
+ low_f: daily.temperature_2m_min[i],
+ precip_in: daily.precipitation_sum[i],
+ rain_probability_pct: daily.precipitation_probability_max[i],
+ gdd: gddDays[i].toFixed(1),
+ })),
+ gdd_7day_total: totalGDD.toFixed(1),
+ rain_alert_48h: rainAlert,
+ harvest_window: harvestWindow,
+ };
+}
+
+async function toolLookupUSDAsoil({ latitude, longitude }) {
+ // USDA Web Soil Survey SDA REST API
+ const query = `SELECT mapunit.muname, component.compname, component.comppct_r,
+ component.taxorder, component.taxsubgrp, chorizon.texture, chorizon.om_r,
+ chorizon.drainagecl
+ FROM mapunit
+ INNER JOIN component ON mapunit.mukey = component.mukey
+ INNER JOIN chorizon ON component.cokey = chorizon.cokey
+ WHERE mu_lks.mukey IN (
+ SELECT * FROM SDA_Get_Mukey_from_intersection_with_WktWgs84(
+ 'point(${longitude} ${latitude})')
+ )
+ AND component.majcompflag = 'Yes'
+ ORDER BY component.comppct_r DESC`;
+
+ try {
+ const res = await fetch('https://sdmdataaccess.sc.egov.usda.gov/TABULAR/post.rest', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: `request=query&query=${encodeURIComponent(query)}&format=JSON`,
+ });
+ const data = await res.json();
+ const rows = data.Table ?? [];
+ return {
+ coordinates: { latitude, longitude },
+ soil_data: rows.slice(0, 5).map((r) => ({
+ map_unit: r[0],
+ component: r[1],
+ percent: r[2],
+ tax_order: r[3],
+ subgroup: r[4],
+ texture: r[5],
+ organic_matter_pct: r[6],
+ drainage_class: r[7],
+ })),
+ };
+ } catch (_) {
+ return {
+ coordinates: { latitude, longitude },
+ note: 'USDA SDA API unavailable β soil data requires network access from background worker',
+ };
+ }
+}
+
+function toolCalculateGDD({ daily_highs, daily_lows, base_temp = 50 }) {
+ const gdd_per_day = daily_highs.map((hi, i) => {
+ const lo = daily_lows[i] ?? hi;
+ const avg = (hi + lo) / 2;
+ return Math.max(0, avg - base_temp);
+ });
+ const total = gdd_per_day.reduce((a, b) => a + b, 0);
+ return {
+ base_temp_f: base_temp,
+ days: gdd_per_day.length,
+ gdd_per_day: gdd_per_day.map((g) => parseFloat(g.toFixed(1))),
+ total_gdd: parseFloat(total.toFixed(1)),
+ interpretation:
+ total < 200 ? 'Early growth stage' :
+ total < 500 ? 'Vegetative growth' :
+ total < 900 ? 'Approaching harvest window' :
+ 'Harvest recommended',
+ };
+}
diff --git a/agrifine-extension/src/sidebar/index.js b/agrifine-extension/src/sidebar/index.js
index 72d1209..2c88fa1 100644
--- a/agrifine-extension/src/sidebar/index.js
+++ b/agrifine-extension/src/sidebar/index.js
@@ -4,6 +4,7 @@ import { DataIngestModule } from '../modules/data-ingest/index.js';
import { FieldProfileModule } from '../modules/field-profile/index.js';
import { DashboardModule } from '../modules/dashboard/index.js';
import { CarbonEstimatorModule } from '../modules/carbon-estimator/index.js';
+import { AgRefineModule } from '../ag-refine/index.js';
import { sessionSet, sessionGet, KEYS } from '../utils/storage.js';
// ββ Module registry βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -13,6 +14,7 @@ const MODULES = [
FieldProfileModule(),
DashboardModule(),
CarbonEstimatorModule(),
+ AgRefineModule(),
];
const moduleMap = Object.fromEntries(MODULES.map((m) => [m.id, m]));
diff --git a/agrifine-extension/src/sidebar/sidebar.html b/agrifine-extension/src/sidebar/sidebar.html
index db557ca..f9a0880 100644
--- a/agrifine-extension/src/sidebar/sidebar.html
+++ b/agrifine-extension/src/sidebar/sidebar.html
@@ -86,6 +86,15 @@
Carbon
+
+