open-research 0.1.26 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,608 @@
1
+ import {
2
+ loadOntology,
3
+ saveOntology
4
+ } from "./chunk-3WM33M3O.js";
5
+ import {
6
+ DEFAULT_CONFIDENCE
7
+ } from "./chunk-KJHM7ZW2.js";
8
+ import {
9
+ getConnections,
10
+ getNote,
11
+ searchNotes
12
+ } from "./chunk-IOR7G25X.js";
13
+ import "./chunk-3RG5ZIWI.js";
14
+
15
+ // src/lib/ontology/write-tools.ts
16
+ function createNote(ontology, params) {
17
+ const noteId = crypto.randomUUID();
18
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19
+ const note = {
20
+ id: noteId,
21
+ content: params.content,
22
+ kind: params.kind,
23
+ confidence: params.confidence ?? DEFAULT_CONFIDENCE[params.kind],
24
+ meta: params.kind === "source" ? params.meta : void 0,
25
+ edges: [],
26
+ createdAt: now,
27
+ updatedAt: now
28
+ };
29
+ ontology.notes.push(note);
30
+ return { ontology, noteId };
31
+ }
32
+ function createEdge(ontology, params) {
33
+ const sourceNote = ontology.notes.find((n) => n.id === params.sourceNoteId);
34
+ if (!sourceNote) {
35
+ throw new Error(`Source note not found: ${params.sourceNoteId}`);
36
+ }
37
+ const targetExists = ontology.notes.some((n) => n.id === params.targetNoteId);
38
+ if (!targetExists) {
39
+ throw new Error(`Target note not found: ${params.targetNoteId}`);
40
+ }
41
+ const duplicate = sourceNote.edges.find(
42
+ (e) => e.targetId === params.targetNoteId && e.relation === params.relation
43
+ );
44
+ if (duplicate) {
45
+ throw new Error(
46
+ `Edge already exists: ${params.sourceNoteId} \u2192 ${params.targetNoteId} (${params.relation})`
47
+ );
48
+ }
49
+ sourceNote.edges.push({
50
+ targetId: params.targetNoteId,
51
+ relation: params.relation,
52
+ strength: params.strength,
53
+ direction: params.direction,
54
+ context: params.context
55
+ });
56
+ sourceNote.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
57
+ const edgeId = `${params.sourceNoteId}\u2192${params.targetNoteId}:${params.relation}`;
58
+ return { ontology, edgeId };
59
+ }
60
+ function updateNote(ontology, params) {
61
+ const note = ontology.notes.find((n) => n.id === params.noteId);
62
+ if (!note) throw new Error(`Note not found: ${params.noteId}`);
63
+ if (params.content !== void 0) note.content = params.content;
64
+ if (params.confidence !== void 0) note.confidence = params.confidence;
65
+ note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
66
+ return ontology;
67
+ }
68
+ function updateEdge(ontology, params) {
69
+ const sourceNote = ontology.notes.find((n) => n.id === params.sourceNoteId);
70
+ if (!sourceNote) throw new Error(`Source note not found: ${params.sourceNoteId}`);
71
+ const edge = sourceNote.edges.find(
72
+ (e) => e.targetId === params.targetNoteId && e.relation === params.relation
73
+ );
74
+ if (!edge) {
75
+ throw new Error(
76
+ `Edge not found: ${params.sourceNoteId} \u2192 ${params.targetNoteId} (${params.relation})`
77
+ );
78
+ }
79
+ if (params.strength !== void 0) edge.strength = params.strength;
80
+ if (params.context !== void 0) edge.context = params.context;
81
+ sourceNote.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
82
+ return ontology;
83
+ }
84
+ function removeEdge(ontology, params) {
85
+ const sourceNote = ontology.notes.find((n) => n.id === params.sourceNoteId);
86
+ if (!sourceNote) throw new Error(`Source note not found: ${params.sourceNoteId}`);
87
+ const idx = sourceNote.edges.findIndex(
88
+ (e) => e.targetId === params.targetNoteId && e.relation === params.relation
89
+ );
90
+ if (idx === -1) {
91
+ throw new Error(
92
+ `Edge not found: ${params.sourceNoteId} \u2192 ${params.targetNoteId} (${params.relation})`
93
+ );
94
+ }
95
+ sourceNote.edges.splice(idx, 1);
96
+ sourceNote.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
97
+ return ontology;
98
+ }
99
+ var STRENGTH_RANK = {
100
+ strong: 3,
101
+ moderate: 2,
102
+ weak: 1
103
+ };
104
+ function mergeNotes(ontology, params) {
105
+ const keepNote = ontology.notes.find((n) => n.id === params.keepNoteId);
106
+ if (!keepNote) throw new Error(`Keep note not found: ${params.keepNoteId}`);
107
+ const removeNote = ontology.notes.find((n) => n.id === params.removeNoteId);
108
+ if (!removeNote) throw new Error(`Remove note not found: ${params.removeNoteId}`);
109
+ let edgesRedirected = 0;
110
+ for (const note of ontology.notes) {
111
+ if (note.id === params.removeNoteId) continue;
112
+ for (const edge of note.edges) {
113
+ if (edge.targetId === params.removeNoteId) {
114
+ edge.targetId = params.keepNoteId;
115
+ edgesRedirected++;
116
+ }
117
+ }
118
+ }
119
+ for (const edge of removeNote.edges) {
120
+ const targetId = edge.targetId === params.removeNoteId ? params.keepNoteId : edge.targetId;
121
+ if (targetId === params.keepNoteId) continue;
122
+ const existing = keepNote.edges.find(
123
+ (e) => e.targetId === targetId && e.relation === edge.relation
124
+ );
125
+ if (existing) {
126
+ if (STRENGTH_RANK[edge.strength] > STRENGTH_RANK[existing.strength]) {
127
+ existing.strength = edge.strength;
128
+ existing.context = edge.context;
129
+ }
130
+ } else {
131
+ keepNote.edges.push({ ...edge, targetId });
132
+ }
133
+ }
134
+ const edgeMap = /* @__PURE__ */ new Map();
135
+ for (const edge of keepNote.edges) {
136
+ const key = `${edge.targetId}:${edge.relation}`;
137
+ const existing = edgeMap.get(key);
138
+ if (!existing || STRENGTH_RANK[edge.strength] > STRENGTH_RANK[existing.strength]) {
139
+ edgeMap.set(key, edge);
140
+ }
141
+ }
142
+ keepNote.edges = [...edgeMap.values()];
143
+ if (params.mergedContent) {
144
+ keepNote.content = params.mergedContent;
145
+ }
146
+ keepNote.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
147
+ ontology.notes = ontology.notes.filter((n) => n.id !== params.removeNoteId);
148
+ return { ontology, edgesRedirected };
149
+ }
150
+
151
+ // src/lib/ontology/manager.ts
152
+ var SYSTEM_PROMPT = `You are the Ontology Manager. You maintain the structured knowledge graph for a research project.
153
+
154
+ # Your single job
155
+
156
+ After each conversation turn, you receive the researcher's message, the AI assistant's response, and any tool outputs. You decide what substantive knowledge to extract and how to structure it in the ontology.
157
+
158
+ You output ONLY tool calls. No conversational text. When you're done, stop calling tools \u2014 that signals completion.
159
+
160
+ # Decision process
161
+
162
+ For each turn, follow this sequence:
163
+
164
+ 1. READ the turn. Identify any substantive knowledge: new sources, findings, claims, methods, questions, or insights.
165
+ 2. SKIP if the turn contains only chitchat, UI commands, greetings, or meta-conversation. Outputting 0 operations is correct.
166
+ 3. SEARCH before writing. For every piece of knowledge you want to capture, first call search_notes to check if it already exists. Duplicate notes degrade the entire system.
167
+ 4. WRITE only what's new. Create notes, add edges, or update existing notes. Typical turn: 1-5 operations.
168
+
169
+ # Note kinds
170
+
171
+ | Kind | What it captures | Required edges |
172
+ |------|-----------------|----------------|
173
+ | source | A citable origin: paper, URL, dataset, book | None |
174
+ | finding | A specific result from a source | MUST have derived-from \u2192 source |
175
+ | claim | An argument or assertion in the research | None (but should accumulate supports/contradicts over time) |
176
+ | question | An open gap, uncertainty, or research question | None |
177
+ | method | A methodology or analytical technique | None |
178
+ | insight | A synthesis connecting multiple findings/claims | SHOULD have derived-from \u2192 the findings/claims it synthesizes |
179
+
180
+ # Edge creation \u2014 decision tree
181
+
182
+ When connecting two notes, use this decision tree:
183
+
184
+ Is note A direct provenance for note B? (A was extracted from B, or B was synthesized from A)
185
+ \u2192 YES: derived-from (directed, A \u2192 B)
186
+
187
+ Does note A provide direct empirical evidence that tests what note B claims?
188
+ \u2192 YES: supports (directed, A \u2192 B)
189
+ \u2192 A is merely about the same topic as B: relates-to (NOT supports)
190
+
191
+ Do notes A and B assert things that CANNOT BOTH BE TRUE about the same variable, measured in comparable conditions?
192
+ \u2192 YES: contradicts (mutual)
193
+ \u2192 They measure different things, or use different conditions: relates-to (NOT contradicts)
194
+
195
+ None of the above, but genuinely connected?
196
+ \u2192 relates-to (mutual)
197
+
198
+ ## Edge context \u2014 good vs bad
199
+
200
+ The context field explains WHY the connection exists. It must add information beyond the relation type.
201
+
202
+ GOOD:
203
+ - "Table 3 reports 12% BLEU improvement on WMT14 EN-DE (p<0.01), directly testing the scaling hypothesis under identical conditions"
204
+ - "Chen used 10x larger training data and observed inverse scaling, suggesting the original finding is dataset-size-dependent"
205
+ - "Synthesized from the sample-size observation (Smith) and the scaling curve (Jones) to hypothesize diminishing returns above 10B parameters"
206
+
207
+ BAD:
208
+ - "supports the claim" (redundant \u2014 the relation field already says this)
209
+ - "related" (says nothing)
210
+ - "from this paper" (the edge already points to the source)
211
+
212
+ # Confidence
213
+
214
+ Only change confidence when the conversation provides EXPLICIT evidence. Do not infer from tone or speculation. When uncertain, leave unchanged.
215
+
216
+ Defaults: source/finding/method \u2192 established | claim/insight \u2192 hypothesized | question \u2192 questioned
217
+
218
+ # Critical constraint
219
+
220
+ A wrong edge is worse than a missing edge. Missing edges can be added later. Wrong edges actively mislead the researcher's analysis. When uncertain about an edge type, use relates-to with explanatory context, or skip the edge entirely.`;
221
+ var MANAGER_TOOLS = [
222
+ // ── Read tools ───────────────────────────────────────────────────────
223
+ {
224
+ type: "function",
225
+ function: {
226
+ name: "get_note",
227
+ description: "Retrieve a single note by ID. Returns its content, kind, confidence, all edges, and source metadata. Use when you already have a note ID from search results or edge targets.",
228
+ parameters: {
229
+ type: "object",
230
+ properties: {
231
+ noteId: { type: "string", description: "UUID of the note to retrieve" }
232
+ },
233
+ required: ["noteId"],
234
+ additionalProperties: false
235
+ }
236
+ }
237
+ },
238
+ {
239
+ type: "function",
240
+ function: {
241
+ name: "search_notes",
242
+ description: "Find notes by text and/or structure. ALWAYS call this before create_note to check for duplicates. Combine text queries with structural filters for precise results. Example: find unsupported claims \u2192 { kind: 'claim', missingEdge: 'supports' }. Example: find a paper \u2192 { queries: ['Smith 2024', 'attention scaling'], kind: 'source' }.",
243
+ parameters: {
244
+ type: "object",
245
+ properties: {
246
+ queries: {
247
+ type: "array",
248
+ items: { type: "string" },
249
+ description: "Text search phrases (OR logic). Include 2-3 phrasings to cover synonyms. E.g. ['transformer efficiency', 'attention scaling', 'BLEU improvement']"
250
+ },
251
+ kind: { type: "string", enum: ["source", "finding", "claim", "question", "method", "insight"] },
252
+ confidence: { type: "string", enum: ["established", "supported", "hypothesized", "questioned", "refuted"] },
253
+ hasEdge: { type: "string", enum: ["supports", "contradicts", "derived-from", "relates-to"], description: "Note must have at least one outgoing or incoming mutual edge of this type" },
254
+ missingEdge: { type: "string", enum: ["supports", "contradicts", "derived-from", "relates-to"], description: "Note must have NO edges of this type" },
255
+ limit: { type: "number", description: "Max results. Default: 10" }
256
+ },
257
+ additionalProperties: false
258
+ }
259
+ }
260
+ },
261
+ {
262
+ type: "function",
263
+ function: {
264
+ name: "get_connections",
265
+ description: "Explore a note's neighborhood. Returns the note and all notes connected within N hops. Call this BEFORE creating edges to understand what's already connected. Also useful for finding notes that search_notes missed \u2014 follow edges to discover related notes.",
266
+ parameters: {
267
+ type: "object",
268
+ properties: {
269
+ noteId: { type: "string", description: "Starting note ID" },
270
+ depth: { type: "number", description: "Hops to traverse. Default: 1, max: 3. Use 1 for immediate neighbors, 2 for extended context." }
271
+ },
272
+ required: ["noteId"],
273
+ additionalProperties: false
274
+ }
275
+ }
276
+ },
277
+ // ── Write tools ──────────────────────────────────────────────────────
278
+ {
279
+ type: "function",
280
+ function: {
281
+ name: "create_note",
282
+ description: "Create a new note in the ontology. PREREQUISITE: You must call search_notes first to verify this note doesn't already exist. If a similar note exists, use update_note or create_edge instead. For findings: you MUST create a derived-from edge to the source immediately after.",
283
+ parameters: {
284
+ type: "object",
285
+ properties: {
286
+ content: { type: "string", description: "Clear, factual description in 1-3 sentences. Be specific \u2014 include numbers, conditions, and qualifiers." },
287
+ kind: { type: "string", enum: ["source", "finding", "claim", "question", "method", "insight"] },
288
+ confidence: { type: "string", enum: ["established", "supported", "hypothesized", "questioned", "refuted"], description: "Omit to use default for the kind" },
289
+ meta: {
290
+ type: "object",
291
+ description: "Citation metadata. Only for kind: 'source'. Include as many fields as available.",
292
+ properties: {
293
+ authors: { type: "string", description: "Author names, e.g. 'Vaswani, Shazeer, Parmar, et al.'" },
294
+ year: { type: "number" },
295
+ venue: { type: "string", description: "Publication venue, e.g. 'NeurIPS 2017'" },
296
+ url: { type: "string" },
297
+ doi: { type: "string" },
298
+ filePath: { type: "string", description: "Workspace-relative path to the file, e.g. 'sources/vaswani-2017.pdf'" }
299
+ },
300
+ additionalProperties: false
301
+ }
302
+ },
303
+ required: ["content", "kind"],
304
+ additionalProperties: false
305
+ }
306
+ }
307
+ },
308
+ {
309
+ type: "function",
310
+ function: {
311
+ name: "create_edge",
312
+ description: "Connect two notes with a typed, directional relationship. Follow the edge decision tree in your instructions. Key rules: supports/derived-from are always 'directed'. contradicts/relates-to are always 'mutual'. The context field must explain WHY \u2014 not just restate the relation.",
313
+ parameters: {
314
+ type: "object",
315
+ properties: {
316
+ sourceNoteId: { type: "string", description: "ID of the FROM note" },
317
+ targetNoteId: { type: "string", description: "ID of the TO note" },
318
+ relation: { type: "string", enum: ["supports", "contradicts", "derived-from", "relates-to"] },
319
+ strength: {
320
+ type: "string",
321
+ enum: ["strong", "moderate", "weak"],
322
+ description: "strong = direct, unambiguous connection. moderate = relevant but indirect. weak = tangential."
323
+ },
324
+ direction: {
325
+ type: "string",
326
+ enum: ["directed", "mutual"],
327
+ description: "directed: supports, derived-from (A\u2192B only). mutual: contradicts, relates-to (bidirectional)."
328
+ },
329
+ context: {
330
+ type: "string",
331
+ description: "Explain WHY this connection exists with specific details. Must add information beyond the relation type. Include page numbers, table references, conditions, or reasoning."
332
+ }
333
+ },
334
+ required: ["sourceNoteId", "targetNoteId", "relation", "strength", "direction", "context"],
335
+ additionalProperties: false
336
+ }
337
+ }
338
+ },
339
+ {
340
+ type: "function",
341
+ function: {
342
+ name: "update_note",
343
+ description: "Modify a note's content or confidence. Use when new information refines, extends, or corrects an existing note rather than creating a duplicate.",
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ noteId: { type: "string" },
348
+ content: { type: "string", description: "Replacement content. Omit to keep current." },
349
+ confidence: { type: "string", enum: ["established", "supported", "hypothesized", "questioned", "refuted"], description: "New confidence level. Only change with explicit evidence. Omit to keep current." }
350
+ },
351
+ required: ["noteId"],
352
+ additionalProperties: false
353
+ }
354
+ }
355
+ },
356
+ {
357
+ type: "function",
358
+ function: {
359
+ name: "update_edge",
360
+ description: "Modify an existing edge's strength or context. Use when new evidence changes the strength of a connection, or when context needs to be enriched.",
361
+ parameters: {
362
+ type: "object",
363
+ properties: {
364
+ sourceNoteId: { type: "string" },
365
+ targetNoteId: { type: "string" },
366
+ relation: { type: "string", enum: ["supports", "contradicts", "derived-from", "relates-to"], description: "Identifies which edge to update (source + target + relation is unique)" },
367
+ strength: { type: "string", enum: ["strong", "moderate", "weak"] },
368
+ context: { type: "string" }
369
+ },
370
+ required: ["sourceNoteId", "targetNoteId", "relation"],
371
+ additionalProperties: false
372
+ }
373
+ }
374
+ },
375
+ {
376
+ type: "function",
377
+ function: {
378
+ name: "remove_edge",
379
+ description: "Delete an edge that was created incorrectly. Only use when the relationship is definitively wrong \u2014 not just weak.",
380
+ parameters: {
381
+ type: "object",
382
+ properties: {
383
+ sourceNoteId: { type: "string" },
384
+ targetNoteId: { type: "string" },
385
+ relation: { type: "string", enum: ["supports", "contradicts", "derived-from", "relates-to"] }
386
+ },
387
+ required: ["sourceNoteId", "targetNoteId", "relation"],
388
+ additionalProperties: false
389
+ }
390
+ }
391
+ },
392
+ {
393
+ type: "function",
394
+ function: {
395
+ name: "merge_notes",
396
+ description: "Combine two duplicate notes into one. All edges pointing to the removed note are redirected to the kept note. Duplicate edges are deduplicated (strongest wins). Use when search reveals the same knowledge was captured twice.",
397
+ parameters: {
398
+ type: "object",
399
+ properties: {
400
+ keepNoteId: { type: "string", description: "ID of the note to keep (usually the more complete one)" },
401
+ removeNoteId: { type: "string", description: "ID of the note to remove \u2014 its edges will be redirected" },
402
+ mergedContent: { type: "string", description: "Optional: replacement content that combines both notes' information" }
403
+ },
404
+ required: ["keepNoteId", "removeNoteId"],
405
+ additionalProperties: false
406
+ }
407
+ }
408
+ }
409
+ ];
410
+ function executeManagerTool(name, args, ontology) {
411
+ try {
412
+ switch (name) {
413
+ // Read tools
414
+ case "get_note": {
415
+ const note = getNote(ontology, String(args.noteId));
416
+ if (!note) return { result: `Note not found: ${args.noteId}`, ontology };
417
+ return { result: JSON.stringify(note, null, 2), ontology };
418
+ }
419
+ case "search_notes": {
420
+ const results = searchNotes(ontology, {
421
+ queries: args.queries,
422
+ kind: args.kind,
423
+ confidence: args.confidence,
424
+ hasEdge: args.hasEdge,
425
+ missingEdge: args.missingEdge,
426
+ limit: args.limit
427
+ });
428
+ if (results.length === 0) return { result: "No matching notes found.", ontology };
429
+ return {
430
+ result: results.map(
431
+ (n) => `[${n.id}] (${n.kind}, ${n.confidence}) "${n.content}" \u2014 ${n.edges.length} edges`
432
+ ).join("\n"),
433
+ ontology
434
+ };
435
+ }
436
+ case "get_connections": {
437
+ const { root, connected } = getConnections(
438
+ ontology,
439
+ String(args.noteId),
440
+ args.depth ?? 1
441
+ );
442
+ if (!root) return { result: `Note not found: ${args.noteId}`, ontology };
443
+ const lines = [
444
+ `Root: [${root.id}] (${root.kind}) "${root.content}"`,
445
+ `Edges: ${root.edges.map((e) => `${e.relation} \u2192 ${e.targetId.slice(0, 8)}`).join(", ") || "none"}`,
446
+ `Connected (${connected.length}):`,
447
+ ...connected.map((c) => ` [${c.id}] (${c.kind}) "${c.content}"`)
448
+ ];
449
+ return { result: lines.join("\n"), ontology };
450
+ }
451
+ // Write tools
452
+ case "create_note": {
453
+ const { ontology: updated, noteId } = createNote(ontology, {
454
+ content: String(args.content),
455
+ kind: String(args.kind),
456
+ confidence: args.confidence,
457
+ meta: args.meta
458
+ });
459
+ return { result: `Created note: ${noteId}`, ontology: updated };
460
+ }
461
+ case "create_edge": {
462
+ const { ontology: updated, edgeId } = createEdge(ontology, {
463
+ sourceNoteId: String(args.sourceNoteId),
464
+ targetNoteId: String(args.targetNoteId),
465
+ relation: String(args.relation),
466
+ strength: String(args.strength),
467
+ direction: String(args.direction),
468
+ context: String(args.context)
469
+ });
470
+ return { result: `Created edge: ${edgeId}`, ontology: updated };
471
+ }
472
+ case "update_note": {
473
+ const updated = updateNote(ontology, {
474
+ noteId: String(args.noteId),
475
+ content: args.content,
476
+ confidence: args.confidence
477
+ });
478
+ return { result: `Updated note: ${args.noteId}`, ontology: updated };
479
+ }
480
+ case "update_edge": {
481
+ const updated = updateEdge(ontology, {
482
+ sourceNoteId: String(args.sourceNoteId),
483
+ targetNoteId: String(args.targetNoteId),
484
+ relation: String(args.relation),
485
+ strength: args.strength,
486
+ context: args.context
487
+ });
488
+ return { result: `Updated edge.`, ontology: updated };
489
+ }
490
+ case "remove_edge": {
491
+ const updated = removeEdge(ontology, {
492
+ sourceNoteId: String(args.sourceNoteId),
493
+ targetNoteId: String(args.targetNoteId),
494
+ relation: String(args.relation)
495
+ });
496
+ return { result: `Removed edge.`, ontology: updated };
497
+ }
498
+ case "merge_notes": {
499
+ const { ontology: updated, edgesRedirected } = mergeNotes(ontology, {
500
+ keepNoteId: String(args.keepNoteId),
501
+ removeNoteId: String(args.removeNoteId),
502
+ mergedContent: args.mergedContent
503
+ });
504
+ return { result: `Merged. ${edgesRedirected} edges redirected.`, ontology: updated };
505
+ }
506
+ default:
507
+ return { result: `Unknown tool: ${name}`, ontology };
508
+ }
509
+ } catch (err) {
510
+ return { result: `Error: ${err?.message ?? err}`, ontology };
511
+ }
512
+ }
513
+ var MAX_TOOL_OUTPUT_CHARS = 32e3;
514
+ function truncateToolOutputs(outputs) {
515
+ if (outputs.length === 0) return "(no tool calls this turn)";
516
+ let total = 0;
517
+ const included = [];
518
+ for (let i = outputs.length - 1; i >= 0; i--) {
519
+ const entry = outputs[i];
520
+ const size = entry.tool.length + entry.output.length;
521
+ if (total + size > MAX_TOOL_OUTPUT_CHARS && included.length > 0) break;
522
+ included.unshift(entry);
523
+ total += size;
524
+ }
525
+ return included.map((o) => `[${o.tool}] ${o.output.slice(0, 4e3)}`).join("\n\n");
526
+ }
527
+ var MAX_ITERATIONS = 10;
528
+ async function runOntologyManager(input) {
529
+ let ontology = await loadOntology(input.workspaceDir);
530
+ const turnSummary = [
531
+ `## Conversation Turn`,
532
+ ``,
533
+ `**User:** ${input.userMessage.slice(0, 3e3)}`,
534
+ ``,
535
+ `**Agent response:** ${input.agentResponse.slice(0, 3e3)}`,
536
+ ``,
537
+ `**Tool outputs:**`,
538
+ truncateToolOutputs(input.toolOutputs)
539
+ ].join("\n");
540
+ const messages = [
541
+ { role: "system", content: SYSTEM_PROMPT },
542
+ { role: "user", content: turnSummary }
543
+ ];
544
+ let mutated = false;
545
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
546
+ let fullText = "";
547
+ let toolCalls = [];
548
+ for await (const chunk of input.provider.callLLMStreaming({
549
+ messages,
550
+ tools: MANAGER_TOOLS,
551
+ model: "gpt-5.4"
552
+ })) {
553
+ if (chunk.type === "text_delta") {
554
+ fullText += chunk.content;
555
+ } else if (chunk.type === "done") {
556
+ toolCalls = chunk.toolCalls;
557
+ }
558
+ }
559
+ if (toolCalls.length === 0) break;
560
+ messages.push({
561
+ role: "assistant",
562
+ content: fullText || null,
563
+ tool_calls: toolCalls.map((tc) => ({
564
+ id: tc.id,
565
+ type: "function",
566
+ function: { name: tc.name, arguments: tc.arguments }
567
+ }))
568
+ });
569
+ for (const toolCall of toolCalls) {
570
+ const args = JSON.parse(toolCall.arguments || "{}");
571
+ const { result, ontology: updated } = executeManagerTool(
572
+ toolCall.name,
573
+ args,
574
+ ontology
575
+ );
576
+ if (updated !== ontology) mutated = true;
577
+ ontology = updated;
578
+ messages.push({
579
+ role: "tool",
580
+ tool_call_id: toolCall.id,
581
+ content: result
582
+ });
583
+ }
584
+ }
585
+ if (mutated) {
586
+ await saveOntology(ontology, input.workspaceDir);
587
+ }
588
+ }
589
+
590
+ // src/lib/ontology/manager-queue.ts
591
+ var pendingWrite = Promise.resolve();
592
+ function enqueueOntologyManager(input) {
593
+ pendingWrite = pendingWrite.then(() => runOntologyManager({
594
+ userMessage: input.userMessage,
595
+ agentResponse: input.agentResponse,
596
+ toolOutputs: input.toolOutputs,
597
+ provider: input.provider,
598
+ workspaceDir: input.workspaceDir
599
+ })).then(() => input.onOntologyUpdated?.()).catch((err) => {
600
+ if (process.env.DEBUG) {
601
+ process.stderr.write(`[ontology-manager] Error: ${err?.message ?? err}
602
+ `);
603
+ }
604
+ });
605
+ }
606
+ export {
607
+ enqueueOntologyManager
608
+ };