This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Rewrite island:analyze with letta-code-sdk agent

Replace rule-based keyword matching with LLM-powered analysis:
- Agent receives full island context (vertices, edges, domains, contributors)
- Agent calls island_save tool with structured analysis
- Produces real narrative synthesis instead of template strings
- Removes carry dependency — works from discovered component data alone
- Keeps same SQLite schema, CLI flags, and output format

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

author
nandi
co-author
Letta Code
date (May 15, 2026, 5:31 AM UTC) commit b81ec113 parent 219156b7
+233 -299
+233 -299
src/island-analyze.ts
··· 1 1 /** 2 2 * island:analyze <island-id> 3 3 * 4 - * Analyze a discovered component against carry data. 4 + * Analyze a discovered component using an LLM agent (letta-code-sdk). 5 5 * Writes analysis results to SQLite (not PDS — use publish for that). 6 6 * 7 7 * Usage: ··· 14 14 15 15 import { resolve, dirname } from 'node:path'; 16 16 import { fileURLToPath } from 'node:url'; 17 - import { execFileSync } from 'node:child_process'; 18 17 import { Database } from 'bun:sqlite'; 18 + import { 19 + createAgent, 20 + createSession, 21 + type AnyAgentTool, 22 + type AgentToolResult, 23 + type SDKResultMessage, 24 + } from '@letta-ai/letta-code-sdk'; 19 25 import { loadDotEnv } from './cli-utils.js'; 20 - import { findComponents, islandId, domainFromUrl, type Component } from './island-shared.js'; 26 + import { findComponents, domainFromUrl, type Component, type EdgeRow, fetchComponentEdges, resolveDidHandles } from './island-shared.js'; 21 27 22 28 const __dirname = dirname(fileURLToPath(import.meta.url)); 23 29 await loadDotEnv(resolve(__dirname, '..', '.env')); ··· 26 32 const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db'); 27 33 28 34 // --- Types --- 29 - 30 - interface CarryEntity { name: string; url: string; description: string } 31 - interface CarryCitation { url: string; title: string; takeaway: string; domain: string } 32 - interface CarryConnection { source: string; target: string; relation: string; context: string } 33 35 34 36 interface AnalysisResult { 35 37 islandId: string; ··· 66 68 return db; 67 69 } 68 70 69 - // --- Carry data --- 70 - 71 - function carryQueryJson<T>(domain: string, fields: string[]): T[] { 72 - try { 73 - const out = execFileSync('carry', ['query', domain, ...fields, '--format', 'json'], { 74 - cwd: CARRY_DIR, 75 - encoding: 'utf-8', 76 - timeout: 15_000, 77 - }); 78 - return JSON.parse(out) as T[]; 79 - } catch { 80 - return []; 81 - } 82 - } 83 - 84 - function buildEntityUrlIndex(): Map<string, string> { 85 - const entities = carryQueryJson<CarryEntity>('org.latha.entity', ['name', 'url']); 86 - const index = new Map<string, string>(); 87 - for (const e of entities) { 88 - if (e.name && e.url?.startsWith('https://')) { 89 - index.set(e.name, e.url); 90 - } 91 - } 92 - return index; 93 - } 94 - 95 - // --- Analysis --- 96 - 97 - const THEME_KEYWORDS: Record<string, string[]> = { 98 - 'open science': ['open science', 'open access', 'oa', 'preprint'], 99 - 'data sharing': ['data sharing', 'shared data', 'open data', 'dataset'], 100 - 'reproducibility': ['reproducib', 'replicab', 'replication'], 101 - 'decentralization': ['decentraliz', 'federat', 'self-host'], 102 - 'knowledge graphs': ['knowledge graph', 'ontolog', 'linked data', 'semantic'], 103 - 'ai/ml': ['machine learning', 'deep learning', 'neural', 'llm', 'gpt', 'claude'], 104 - 'community': ['community', 'collective', 'cooperative', 'commons'], 105 - 'governance': ['governance', 'policy', 'regulation', 'standards'], 106 - 'infrastructure': ['infrastructure', 'platform', 'tooling', 'pipeline'], 107 - 'stigmergy': ['stigmerg', 'pheromone', 'emergent', 'self-organiz'], 108 - 'provenance': ['provenance', 'lineage', 'traceability', 'attribution'], 109 - 'doi': ['doi', 'zenodo', 'citeable', 'citation'], 110 - 'privacy': ['privacy', 'encryption', 'e2ee', 'consent'], 111 - 'sovereignty': ['sovereignty', 'self-determin', 'autonomy', 'agency'], 112 - 'at protocol': ['atproto', 'at protocol', 'bluesky', 'pds', 'appview'], 113 - 'semantic web': ['semantic web', 'rdf', 'sparql', 'linked data'], 114 - }; 115 - 116 - function extractThemes(text: string): string[] { 117 - const lower = text.toLowerCase(); 118 - return Object.entries(THEME_KEYWORDS) 119 - .filter(([_, terms]) => terms.some(t => lower.includes(t))) 120 - .map(([theme]) => theme); 121 - } 122 - 123 - function matchCarryConnections( 124 - islandUrls: Set<string>, 125 - carryConnections: CarryConnection[], 126 - entityUrlIndex: Map<string, string>, 127 - ): Array<{ source: string; target: string; connectionType: string; note: string }> { 128 - const connections: Array<{ source: string; target: string; connectionType: string; note: string }> = []; 129 - 130 - for (const conn of carryConnections) { 131 - const srcUrl = entityUrlIndex.get(conn.source) || (conn.source.startsWith('https://') ? conn.source : ''); 132 - const tgtUrl = entityUrlIndex.get(conn.target) || (conn.target.startsWith('https://') ? conn.target : ''); 133 - 134 - if (!srcUrl && !tgtUrl) continue; 135 - 136 - const srcInIsland = srcUrl && islandUrls.has(srcUrl); 137 - const tgtInIsland = tgtUrl && islandUrls.has(tgtUrl); 138 - 139 - // Both in island → already connected 140 - if (srcInIsland && tgtInIsland) continue; 141 - 142 - // One side in island → bridge connection 143 - if (srcInIsland && tgtUrl) { 144 - connections.push({ 145 - source: srcUrl, target: tgtUrl, 146 - connectionType: conn.relation, 147 - note: conn.context || `${conn.source} → ${conn.relation} → ${conn.target}`, 148 - }); 149 - } else if (tgtInIsland && srcUrl) { 150 - connections.push({ 151 - source: tgtUrl, target: srcUrl, 152 - connectionType: conn.relation, 153 - note: conn.context || `${conn.source} → ${conn.relation} → ${conn.target}`, 154 - }); 155 - } else if (srcUrl && tgtUrl) { 156 - // Neither in island — nearby carry connection 157 - connections.push({ 158 - source: srcUrl, target: tgtUrl, 159 - connectionType: conn.relation, 160 - note: conn.context || `${conn.source} → ${conn.relation} → ${conn.target}`, 161 - }); 162 - } 163 - } 164 - 165 - // Deduplicate 166 - const seen = new Set<string>(); 167 - return connections.filter(c => { 168 - const key = `${c.source}|${c.target}`; 169 - if (seen.has(key)) return false; 170 - seen.add(key); 171 - return true; 172 - }).slice(0, 50); 173 - } 174 - 175 - function findHotEdges( 176 - db: Database, 177 - islandUrls: Set<string>, 178 - newConnections: Array<{ source: string; target: string }>, 179 - ): string[] { 180 - const highlights: string[] = []; 181 - 182 - const newEndpoints = new Set<string>(); 183 - for (const nc of newConnections) { 184 - newEndpoints.add(nc.source); 185 - newEndpoints.add(nc.target); 186 - } 187 - 188 - // Get all edges touching this component 189 - const edgeRows = db.query('SELECT source, target, did FROM edges').all() as Array<{ source: string; target: string; did: string }>; 190 - const scored: Array<{ source: string; target: string; score: number }> = []; 191 - 192 - for (const row of edgeRows) { 193 - if (!islandUrls.has(row.source) && !islandUrls.has(row.target)) continue; 194 - let score = 0; 195 - if (newEndpoints.has(row.source) || newEndpoints.has(row.target)) score += 3; 196 - if (row.source.startsWith('https://') && row.target.startsWith('https://')) score += 1; 197 - if (score > 0) scored.push({ source: row.source, target: row.target, score }); 198 - } 199 - 200 - scored.sort((a, b) => b.score - a.score); 201 - for (const s of scored.slice(0, 10)) { 202 - highlights.push(`${s.source} → ${s.target}`); 203 - } 204 - 205 - return highlights; 206 - } 207 - 208 - function findTensions(islandText: string, carryCitations: CarryCitation[]): string[] { 209 - const tensions: string[] = []; 210 - const lower = islandText.toLowerCase(); 211 - 212 - for (const c of carryCitations) { 213 - if (!c.takeaway) continue; 214 - const t = c.takeaway.toLowerCase(); 215 - if (t.includes('contradict') || t.includes('however') || t.includes('tension') || t.includes('but')) { 216 - // Check if citation URL is in the island 217 - if ((c.url && lower.includes(c.url.toLowerCase())) || (c.domain && lower.includes(c.domain.toLowerCase()))) { 218 - tensions.push(c.takeaway.slice(0, 500)); 219 - } 220 - } 221 - } 222 - 223 - return tensions.slice(0, 5); 224 - } 225 - 226 - function generateOpenQuestions( 227 - nodeCount: number, 228 - edgeCount: number, 229 - themes: string[], 230 - tensions: string[], 231 - newConnections: Array<{ source: string; target: string }>, 232 - ): string[] { 233 - const questions: string[] = []; 234 - 235 - if (newConnections.length > 0) { 236 - questions.push(`${newConnections.length} carry touchpoints found. How does your existing knowledge reshape this island?`); 237 - } 238 - 239 - if (tensions.length > 0) { 240 - questions.push(`What evidence would resolve the ${tensions.length} tension(s) identified?`); 241 - } 242 - 243 - const edgeTypes = new Set(newConnections.map(c => c.connectionType)); 244 - if (edgeTypes.size > 1) { 245 - questions.push(`This island has ${edgeTypes.size} connection types. What's the dominant relationship?`); 246 - } 247 - 248 - if (nodeCount > 5) { 249 - questions.push(`This is a ${nodeCount}-vertex cluster. Are there sub-clusters within it?`); 250 - } 251 - 252 - if (themes.includes('doi') || themes.includes('open science')) { 253 - questions.push('What data artifacts does this cluster produce, and are they in Zenodo?'); 254 - } 255 - 256 - return questions.slice(0, 5); 257 - } 258 - 259 - function synthesize( 260 - nodeCount: number, 261 - edgeCount: number, 262 - didCount: number, 263 - themes: string[], 264 - highlights: string[], 265 - tensions: string[], 266 - openQuestions: string[], 267 - newConnections: Array<{ source: string; target: string }>, 268 - ): string { 269 - const clusterDesc = `${nodeCount} vertices, ${edgeCount} edges, ${didCount} DIDs`; 270 - const themeStr = themes.length > 0 ? `Themes: ${themes.join(', ')}.` : ''; 271 - const touchpointStr = newConnections.length > 0 272 - ? `${newConnections.length} carry touchpoints — new edges connecting this island to your knowledge base.` 273 - : 'No carry touchpoints found.'; 274 - const highlightStr = highlights.length > 0 275 - ? `${highlights.length} existing edges flagged as semantically significant.` 276 - : ''; 277 - const tensionStr = tensions.length > 0 278 - ? `There ${tensions.length === 1 ? 'is 1 tension' : `are ${tensions.length} tensions`} with your existing claims.` 279 - : ''; 280 - const questionStr = openQuestions.length > 0 281 - ? `Open questions: ${openQuestions.join(' ')}` 282 - : ''; 283 - 284 - return [`Island (${clusterDesc}).`, themeStr, touchpointStr, highlightStr, tensionStr, questionStr].filter(Boolean).join(' '); 285 - } 286 - 287 - // --- Analyze a single component --- 288 - 289 - function analyzeComponent( 290 - db: Database, 291 - comp: Component, 292 - carryConnections: CarryConnection[], 293 - entityUrlIndex: Map<string, string>, 294 - carryCitations: CarryCitation[], 295 - ): AnalysisResult { 296 - const islandUrls = comp.nodes; 297 - 298 - // Build text corpus from node URLs for theme extraction 299 - const islandText = [...islandUrls].join(' '); 300 - 301 - // Extract themes 302 - const themes = extractThemes(islandText); 303 - 304 - // Match carry connections 305 - const newConnections = matchCarryConnections(islandUrls, carryConnections, entityUrlIndex); 306 - 307 - // Find hot edges 308 - const highlights = findHotEdges(db, islandUrls, newConnections); 309 - 310 - // Find tensions 311 - const tensions = findTensions(islandText, carryCitations); 312 - 313 - // Count edges for this component 314 - const allEdges = db.query('SELECT source, target FROM edges').all() as Array<{ source: string; target: string }>; 315 - const edgeCount = allEdges.filter(e => islandUrls.has(e.source) || islandUrls.has(e.target)).length; 316 - 317 - // Open questions 318 - const openQuestions = generateOpenQuestions(islandUrls.size, edgeCount, themes, tensions, newConnections); 319 - 320 - // Synthesis 321 - const synthesis = synthesize( 322 - islandUrls.size, edgeCount, comp.dids.size, 323 - themes, highlights, tensions, openQuestions, newConnections, 324 - ); 325 - 326 - // Title: first theme or first few words of synthesis 327 - const title = themes.length > 0 328 - ? themes.slice(0, 3).map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(', ') 329 - : synthesis.slice(0, 200); 330 - 331 - return { 332 - islandId: comp.islandId, 333 - lexmin: comp.lexmin, 334 - title, 335 - themes, 336 - highlights, 337 - tensions, 338 - openQuestions, 339 - synthesis, 340 - newConnections, 341 - analyzedAt: new Date().toISOString(), 342 - }; 343 - } 344 - 345 - // --- Save analysis to SQLite --- 346 - 347 71 function saveAnalysis(db: Database, result: AnalysisResult): void { 348 72 db.query(` 349 73 INSERT INTO analyses (island_id, title, themes, highlights, tensions, open_questions, synthesis, new_connections, analyzed_at) ··· 365 89 ); 366 90 } 367 91 92 + function loadAnalysis(db: Database, islandId: string): AnalysisResult | null { 93 + const row = db.query('SELECT * FROM analyses WHERE island_id = ?').get(islandId) as Record<string, string> | null; 94 + if (!row) return null; 95 + return { 96 + islandId: row.island_id, 97 + lexmin: '', 98 + title: row.title ?? '', 99 + themes: JSON.parse(row.themes ?? '[]'), 100 + highlights: JSON.parse(row.highlights ?? '[]'), 101 + tensions: JSON.parse(row.tensions ?? '[]'), 102 + openQuestions: JSON.parse(row.open_questions ?? '[]'), 103 + synthesis: row.synthesis ?? '', 104 + newConnections: JSON.parse(row.new_connections ?? '[]'), 105 + analyzedAt: row.analyzed_at, 106 + }; 107 + } 108 + 109 + // --- Build island context from SQLite data --- 110 + 111 + function buildIslandContext(db: Database, comp: Component): string { 112 + const edges = fetchComponentEdges(db, comp); 113 + const didHandles = resolveDidHandles(db, comp.dids); 114 + 115 + // Domain distribution 116 + const domainCounts = new Map<string, number>(); 117 + for (const url of comp.nodes) { 118 + const d = domainFromUrl(url); 119 + domainCounts.set(d, (domainCounts.get(d) ?? 0) + 1); 120 + } 121 + const topDomains = [...domainCounts.entries()] 122 + .sort((a, b) => b[1] - a[1]) 123 + .slice(0, 15) 124 + .map(([d, c]) => `${d} (${c})`) 125 + .join(', '); 126 + 127 + // Edge type distribution 128 + const edgeTypes = new Map<string, number>(); 129 + for (const e of edges) { 130 + const t = e.relation || 'relates'; 131 + edgeTypes.set(t, (edgeTypes.get(t) ?? 0) + 1); 132 + } 133 + const edgeTypeStr = [...edgeTypes.entries()] 134 + .sort((a, b) => b[1] - a[1]) 135 + .map(([t, c]) => `${t} (${c})`) 136 + .join(', '); 137 + 138 + // Contributors 139 + const contributorStr = [...comp.dids] 140 + .map((d) => didHandles.get(d) ?? d) 141 + .join(', '); 142 + 143 + // Vertices (grouped by domain for readability) 144 + const verticesByDomain = new Map<string, string[]>(); 145 + for (const url of comp.nodes) { 146 + const d = domainFromUrl(url); 147 + if (!verticesByDomain.has(d)) verticesByDomain.set(d, []); 148 + verticesByDomain.get(d)!.push(url); 149 + } 150 + const vertexSections = [...verticesByDomain.entries()] 151 + .sort((a, b) => b[1].length - a[1].length) 152 + .slice(0, 10) 153 + .map(([domain, urls]) => `${domain}:\n${urls.slice(0, 5).map((u) => ` ${u}`).join('\n')}${urls.length > 5 ? `\n ... and ${urls.length - 5} more` : ''}`) 154 + .join('\n\n'); 155 + 156 + // Edges (sample) 157 + const edgeSample = edges.slice(0, 40) 158 + .map((e) => { 159 + const handle = didHandles.get(e.did) ?? e.did; 160 + return `${domainFromUrl(e.source)} → ${domainFromUrl(e.target)} [${e.relation || 'relates'}] (by ${handle})`; 161 + }) 162 + .join('\n'); 163 + 164 + return [ 165 + `# Island ${comp.islandId}`, 166 + '', 167 + `## Stats`, 168 + `- ${comp.nodes.size} vertices, ${edges.length} edges, ${comp.dids.size} contributors`, 169 + `- Top domains: ${topDomains}`, 170 + `- Edge types: ${edgeTypeStr}`, 171 + `- Contributors: ${contributorStr}`, 172 + '', 173 + `## Vertices`, 174 + vertexSections, 175 + '', 176 + `## Edges (sample)`, 177 + edgeSample, 178 + edges.length > 40 ? `\n... and ${edges.length - 40} more edges` : '', 179 + ].join('\n'); 180 + } 181 + 182 + // --- island_save tool --- 183 + 184 + let pendingResult: AnalysisResult | null = null; 185 + 186 + const islandSaveTool: AnyAgentTool = { 187 + label: 'island_save', 188 + name: 'island_save', 189 + description: `Save your analysis of this island. Call once when you have completed your analysis. 190 + 191 + Fields: 192 + - title: concise sentence capturing the island's core narrative (max 300 chars) 193 + - themes: list of key themes (max 20, each max 100 chars) 194 + - highlights: list of vertex URLs or edge descriptions that are semantically significant (max 20) 195 + - tensions: contradictions or unresolved tensions within the island (max 10, each max 500 chars) 196 + - openQuestions: questions raised by this island (max 10, each max 500 chars) 197 + - synthesis: prose synthesis connecting the island's vertices and edges into a coherent narrative (max 20000 chars) 198 + - newConnections: suggested new edges between vertices, with source URL, target URL, connectionType, and a note explaining why (max 50)`, 199 + parameters: { 200 + type: 'object', 201 + properties: { 202 + title: { type: 'string', description: 'Concise sentence capturing the core narrative' }, 203 + themes: { type: 'array', items: { type: 'string' }, description: 'Key themes' }, 204 + highlights: { type: 'array', items: { type: 'string' }, description: 'Semantically significant vertices or edges' }, 205 + tensions: { type: 'array', items: { type: 'string' }, description: 'Contradictions or unresolved tensions' }, 206 + openQuestions: { type: 'array', items: { type: 'string' }, description: 'Questions raised by this island' }, 207 + synthesis: { type: 'string', description: 'Prose synthesis of the island' }, 208 + newConnections: { 209 + type: 'array', 210 + items: { 211 + type: 'object', 212 + properties: { 213 + source: { type: 'string', description: 'Source vertex URL' }, 214 + target: { type: 'string', description: 'Target vertex URL' }, 215 + connectionType: { type: 'string', description: 'Relationship type (e.g. related-to, supports, contradicts)' }, 216 + note: { type: 'string', description: 'Why this connection exists' }, 217 + }, 218 + required: ['source', 'target', 'connectionType', 'note'], 219 + }, 220 + description: 'Suggested new edges between vertices', 221 + }, 222 + }, 223 + required: ['title', 'themes', 'synthesis'], 224 + }, 225 + execute: async (_id, args): Promise<AgentToolResult<unknown>> => { 226 + const a = args as Record<string, unknown>; 227 + pendingResult = { 228 + islandId: '', 229 + lexmin: '', 230 + title: String(a.title ?? ''), 231 + themes: (a.themes as string[]) ?? [], 232 + highlights: (a.highlights as string[]) ?? [], 233 + tensions: (a.tensions as string[]) ?? [], 234 + openQuestions: (a.openQuestions as string[]) ?? [], 235 + synthesis: String(a.synthesis ?? ''), 236 + newConnections: (a.newConnections as Array<{ source: string; target: string; connectionType: string; note: string }>) ?? [], 237 + analyzedAt: new Date().toISOString(), 238 + }; 239 + return { 240 + content: [{ type: 'text', text: 'Analysis saved.' }], 241 + }; 242 + }, 243 + }; 244 + 368 245 // --- Main --- 369 246 370 247 async function main(): Promise<void> { ··· 375 252 const minSizeArg = args.find((a) => a.startsWith('--min-size=')); 376 253 const minSize = minSizeArg ? Number(minSizeArg.split('=')[1]) : 3; 377 254 378 - // Get island hash from args 379 255 const targetId = args.find((a) => !a.startsWith('--')); 380 256 381 257 if (!targetId && !analyzeAll) { ··· 397 273 process.exit(1); 398 274 } 399 275 400 - console.error('Loading carry data...'); 401 - const carryConnections = carryQueryJson<CarryConnection>('org.latha.connection', ['source', 'target', 'relation', 'context']); 402 - const carryCitations = carryQueryJson<CarryCitation>('org.latha.citation', ['url', 'title', 'takeaway', 'domain']); 403 - const entityUrlIndex = buildEntityUrlIndex(); 404 - console.error(`Carry: ${carryConnections.length} connections, ${carryCitations.length} citations, ${entityUrlIndex.size} entity URLs`); 405 - 406 276 console.error('Finding components...'); 407 277 const components = findComponents(db); 408 278 ··· 420 290 421 291 console.error(`Analyzing ${targets.length} island(s)...`); 422 292 293 + // Create agent 294 + const agentId = await createAgent({ 295 + persona: `You are a research analyst that synthesizes connected components (islands) from a knowledge graph into structured analysis records. 296 + 297 + Given an island — a cluster of URLs connected by semantic edges — you produce: 298 + 1. A title that captures the island's core narrative in one sentence 299 + 2. Themes that describe what this cluster is about 300 + 3. Highlights — vertices or edges that are semantically significant 301 + 4. Tensions — contradictions or unresolved questions within the cluster 302 + 5. Open questions raised by this island 303 + 6. A synthesis — a prose narrative connecting the vertices and edges into a coherent story 304 + 7. New connections — suggested edges between vertices that would strengthen the island 305 + 306 + You always call island_save once when done. You do not ask for permission.`, 307 + human: 'island-analyzer', 308 + tools: [islandSaveTool], 309 + permissionMode: 'bypassPermissions', 310 + memfs: false, 311 + systemInfoReminder: false, 312 + }); 313 + 314 + console.error(`Created agent: ${agentId}`); 315 + 316 + const session = createSession(agentId, { 317 + memfsStartup: 'skip', 318 + }); 319 + 320 + await session.initialize(); 321 + 423 322 const results: AnalysisResult[] = []; 424 323 425 324 for (const comp of targets) { ··· 428 327 const existing = db.query('SELECT analyzed_at FROM analyses WHERE island_id = ?').get(comp.islandId) as { analyzed_at: string } | null; 429 328 if (existing) { 430 329 console.error(`${comp.islandId}: already analyzed at ${existing.analyzed_at} (use --force to re-analyze)`); 330 + const cached = loadAnalysis(db, comp.islandId); 331 + if (cached) { 332 + cached.lexmin = comp.lexmin; 333 + results.push(cached); 334 + } 431 335 continue; 432 336 } 433 337 } 434 338 435 339 console.error(`${comp.islandId}: ${comp.nodes.size} nodes, ${comp.dids.size} DIDs`); 436 - const result = analyzeComponent(db, comp, carryConnections, entityUrlIndex, carryCitations); 437 - saveAnalysis(db, result); 438 - results.push(result); 439 - console.error(` Themes: ${result.themes.join(', ') || 'none'}`); 440 - console.error(` New connections: ${result.newConnections.length}`); 441 - console.error(` Tensions: ${result.tensions.length}`); 340 + 341 + const context = buildIslandContext(db, comp); 342 + pendingResult = null; 343 + 344 + const prompt = `Analyze this island and call island_save with your analysis. 345 + 346 + ${context}`; 347 + 348 + const result: SDKResultMessage = await session.runTurn(prompt); 349 + 350 + if (!result.success) { 351 + console.error(` Agent failed: ${result.error ?? result.errorCode ?? 'unknown'}`); 352 + continue; 353 + } 354 + 355 + if (!pendingResult) { 356 + console.error(` Agent did not call island_save`); 357 + continue; 358 + } 359 + 360 + // Fill in island metadata 361 + pendingResult.islandId = comp.islandId; 362 + pendingResult.lexmin = comp.lexmin; 363 + 364 + saveAnalysis(db, pendingResult); 365 + results.push(pendingResult); 366 + 367 + console.error(` Title: ${pendingResult.title.slice(0, 80)}`); 368 + console.error(` Themes: ${pendingResult.themes.join(', ') || 'none'}`); 369 + console.error(` New connections: ${pendingResult.newConnections.length}`); 370 + console.error(` Tensions: ${pendingResult.tensions.length}`); 442 371 } 372 + 373 + await session[Symbol.asyncDispose](); 443 374 444 375 if (wantJson) { 445 376 console.log(JSON.stringify(results, null, 2)); ··· 462 393 console.log(` Tensions: ${r.tensions.length}`); 463 394 for (const t of r.tensions) console.log(` ${t.slice(0, 100)}`); 464 395 console.log(` Highlights: ${r.highlights.length}`); 465 - console.log(` Synthesis: ${r.synthesis.slice(0, 300)}...`); 396 + for (const h of r.highlights.slice(0, 5)) console.log(` ${h.slice(0, 100)}`); 397 + console.log(` Open questions: ${r.openQuestions.length}`); 398 + for (const q of r.openQuestions) console.log(` ${q.slice(0, 100)}`); 399 + console.log(` Synthesis: ${r.synthesis.slice(0, 300)}${r.synthesis.length > 300 ? '...' : ''}`); 466 400 } 467 401 468 402 db.close();