( sdk: ISdk, kv: StateKV, provider: MemoryProvider, )
| 451 | } |
| 452 | |
| 453 | export function registerGraphFunction( |
| 454 | sdk: ISdk, |
| 455 | kv: StateKV, |
| 456 | provider: MemoryProvider, |
| 457 | ): void { |
| 458 | sdk.registerFunction("mem::graph-extract", |
| 459 | async (data: { observations: CompressedObservation[] }) => { |
| 460 | if (!data.observations || data.observations.length === 0) { |
| 461 | return { success: false, error: "No observations provided" }; |
| 462 | } |
| 463 | |
| 464 | const prompt = buildGraphExtractionPrompt( |
| 465 | data.observations.map((o) => ({ |
| 466 | title: o.title, |
| 467 | narrative: o.narrative, |
| 468 | concepts: o.concepts, |
| 469 | files: o.files, |
| 470 | type: o.type, |
| 471 | })), |
| 472 | ); |
| 473 | |
| 474 | try { |
| 475 | const response = await provider.compress( |
| 476 | GRAPH_EXTRACTION_SYSTEM, |
| 477 | prompt, |
| 478 | ); |
| 479 | |
| 480 | const obsIds = data.observations.map((o) => o.id); |
| 481 | const { nodes, edges } = parseGraphXml(response, obsIds); |
| 482 | |
| 483 | // #814 v2: targeted name-index lookups replace the O(n) scan |
| 484 | // over `kv.list<GraphNode>(KV.graphNodes)`. At 75K nodes the |
| 485 | // list payload exceeds the iii heartbeat budget and the worker |
| 486 | // dies before merge can complete. Each name-index entry is a |
| 487 | // single small kv.get/set pair. |
| 488 | const snap = (await readSnapshot(kv)) ?? emptySnapshot(); |
| 489 | const capturedAt = new Date().toISOString(); |
| 490 | let newNodeCount = 0; |
| 491 | let newEdgeCount = 0; |
| 492 | const newEdgesForTopCheck: GraphEdge[] = []; |
| 493 | |
| 494 | for (const node of nodes) { |
| 495 | const indexKey = nameIndexKey(node.type, node.name); |
| 496 | const existingId = await kv.get<string>( |
| 497 | KV.graphNameIndex, |
| 498 | indexKey, |
| 499 | ); |
| 500 | |
| 501 | let existing: GraphNode | null = null; |
| 502 | if (existingId) { |
| 503 | existing = await kv.get<GraphNode>(KV.graphNodes, existingId); |
| 504 | // #825 follow-up: name-index lookups can resolve into |
| 505 | // pre-reset rows. Drop them so extract writes a fresh |
| 506 | // node + index entry instead of silently reconnecting |
| 507 | // to a legacy orphan (which would keep the snapshot at |
| 508 | // 0 forever after a reset). |
| 509 | if ( |
| 510 | existing && |
no test coverage detected