@oka-core/reason 0.2.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,578 @@
1
+ import { z } from "zod";
2
+ // ─── Formatters ────────────────────────────────────────────────────
3
+ function formatEvents(events) {
4
+ if (events.length === 0)
5
+ return "No prior reasoning found for this area.";
6
+ return events
7
+ .map((e) => `[${e.event_type}] ${e.timestamp}\n${JSON.stringify(e.payload, null, 2)}`)
8
+ .join("\n\n---\n\n");
9
+ }
10
+ function formatContextLearnings(topic, learnings, totalMatched) {
11
+ if (learnings.length === 0) {
12
+ return `No consolidated knowledge found for topic "${topic}". This area may not have enough reasoning events yet.`;
13
+ }
14
+ const lines = [
15
+ `## Institutional Knowledge: ${topic}`,
16
+ ``,
17
+ `> ${totalMatched} learning(s) matched, showing top ${learnings.length}.`,
18
+ ``,
19
+ ];
20
+ for (const l of learnings) {
21
+ const confidence = ((l.confidence ?? 0) * 100).toFixed(0);
22
+ const relevance = ((l.relevance_score ?? 0) * 100).toFixed(0);
23
+ lines.push(`### ${l.summary}`);
24
+ lines.push(`**Relevance:** ${relevance}% | **Confidence:** ${confidence}% | **Category:** ${l.category}`);
25
+ if (l.evidence_count > 1) {
26
+ lines.push(`**Evidence:** ${l.evidence_count} source(s)`);
27
+ }
28
+ if (l.detailed_description) {
29
+ lines.push(`\n${l.detailed_description}`);
30
+ }
31
+ if (l.suggested_action) {
32
+ lines.push(`\n**Suggested action:** ${l.suggested_action}`);
33
+ }
34
+ if (l.file_patterns?.length > 0) {
35
+ lines.push(`**Related files:** ${l.file_patterns.join(", ")}`);
36
+ }
37
+ if (l.tags?.length > 0) {
38
+ lines.push(`**Tags:** ${l.tags.join(", ")}`);
39
+ }
40
+ lines.push(`*Matched by: ${l.metadata.matching_strategy}*`);
41
+ lines.push("");
42
+ }
43
+ return lines.join("\n");
44
+ }
45
+ // ─── Tool registration ─────────────────────────────────────────────
46
+ /**
47
+ * Register read tools on the MCP server.
48
+ *
49
+ * Tools:
50
+ * - context — ranked learnings for a topic (flagship query)
51
+ * - learnings — browse institutional knowledge
52
+ * - decisions — decision log
53
+ * - patterns — recurring patterns
54
+ * - suggest — evidence-ranked priorities
55
+ * - backlog — agentic product backlog
56
+ * - priorities — evidence-ranked priorities from approved backlog
57
+ * - consolidate — manually trigger a consolidation run
58
+ */
59
+ export function registerReadTools(server, client) {
60
+ // ─── context (THE DIFFERENTIATOR) ──────────────────────────────
61
+ server.tool("context", "Get consolidated institutional knowledge about a topic. " +
62
+ "Returns ranked Learnings derived from cross-source consolidation " +
63
+ "(agent reasoning, code, docs, meetings, tickets, chat). " +
64
+ "Each learning has a relevance score, confidence, and evidence count. " +
65
+ "Falls back to raw events if no learnings exist.", {
66
+ topic: z
67
+ .string()
68
+ .describe("Topic to query (e.g., 'auth', 'billing', 'deployment')"),
69
+ repo: z.string().optional().describe("Repository identifier"),
70
+ file_patterns: z
71
+ .string()
72
+ .optional()
73
+ .describe("Comma-separated file patterns to boost matching"),
74
+ limit: z.number().optional().default(10).describe("Max results"),
75
+ min_confidence: z
76
+ .number()
77
+ .optional()
78
+ .default(0.3)
79
+ .describe("Minimum confidence threshold"),
80
+ visibility: z
81
+ .enum(["user_repo", "account_wide", "user_private"])
82
+ .optional()
83
+ .describe("Visibility scope: user_repo (default), account_wide (cross-repo), user_private"),
84
+ }, async (params) => {
85
+ try {
86
+ // Try the new context endpoint first
87
+ const contextResult = await client.getContext(params.topic, {
88
+ file_patterns: params.file_patterns,
89
+ limit: params.limit,
90
+ min_confidence: params.min_confidence,
91
+ visibility: params.visibility,
92
+ });
93
+ if (contextResult.learnings.length > 0) {
94
+ const text = formatContextLearnings(params.topic, contextResult.learnings, contextResult.total_matched);
95
+ return { content: [{ type: "text", text }] };
96
+ }
97
+ // Fall back to raw event query if no learnings exist
98
+ const rawResult = await client.query({
99
+ area: params.topic,
100
+ });
101
+ if (rawResult.results.length > 0) {
102
+ const text = formatEvents(rawResult.results);
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: `## Raw Events: ${params.topic}\n\n> No consolidated learnings yet. Showing ${rawResult.source_count} raw event(s).\n\n${text}`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: `No knowledge found for topic "${params.topic}". This area hasn't been explored yet.`,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ content: [{ type: "text", text: `Error: ${error}` }],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ // ─── learnings ─────────────────────────────────────────────────
129
+ server.tool("learnings", "Browse institutional knowledge. Filter by category, status, " +
130
+ "confidence, or repo. Returns consolidated learnings with evidence counts.", {
131
+ repo: z.string().optional().describe("Repository to filter by"),
132
+ category: z
133
+ .enum([
134
+ "architecture",
135
+ "bug_pattern",
136
+ "convention",
137
+ "decision",
138
+ "performance",
139
+ "security",
140
+ "workflow",
141
+ "domain_knowledge",
142
+ ])
143
+ .optional()
144
+ .describe("Filter by learning category"),
145
+ status: z
146
+ .enum(["active", "reinforced", "contradicted", "obsolete"])
147
+ .optional()
148
+ .describe("Filter by learning status"),
149
+ min_confidence: z
150
+ .number()
151
+ .optional()
152
+ .describe("Minimum confidence (0.0-1.0)"),
153
+ limit: z.number().optional().default(30).describe("Max results"),
154
+ visibility: z
155
+ .enum(["user_repo", "account_wide", "user_private"])
156
+ .optional()
157
+ .describe("Visibility scope: user_repo (default), account_wide (cross-repo), user_private"),
158
+ }, async (params) => {
159
+ try {
160
+ const result = await client.getLearnings({
161
+ repo: params.repo,
162
+ category: params.category,
163
+ status: params.status,
164
+ min_confidence: params.min_confidence,
165
+ limit: params.limit,
166
+ visibility: params.visibility,
167
+ });
168
+ if (!result.learnings || result.learnings.length === 0) {
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: "No learnings found matching the filters. " +
174
+ "Learnings are generated when the consolidation engine processes reasoning events.",
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ const lines = [
180
+ `## Learnings (${result.total} total, showing ${result.learnings.length})`,
181
+ "",
182
+ ];
183
+ for (const l of result.learnings) {
184
+ const confidence = ((l.confidence || 0) * 100).toFixed(0);
185
+ lines.push(`### ${l.summary}`);
186
+ lines.push(`**Confidence:** ${confidence}% | **Category:** ${l.category || "general"} | **Status:** ${l.status || "active"}`);
187
+ if (l.evidence_count > 1) {
188
+ lines.push(`**Evidence:** ${l.evidence_count} source(s)`);
189
+ }
190
+ if (l.suggested_action) {
191
+ lines.push(`**Action:** ${l.suggested_action}`);
192
+ }
193
+ if (l.file_patterns?.length > 0) {
194
+ lines.push(`**Files:** ${l.file_patterns.join(", ")}`);
195
+ }
196
+ lines.push("");
197
+ }
198
+ return {
199
+ content: [{ type: "text", text: lines.join("\n") }],
200
+ };
201
+ }
202
+ catch (error) {
203
+ return {
204
+ content: [{ type: "text", text: `Error: ${error}` }],
205
+ isError: true,
206
+ };
207
+ }
208
+ });
209
+ // ─── decisions ─────────────────────────────────────────────────
210
+ server.tool("decisions", "Get the decision log for a repo, optionally filtered by module and timeframe", {
211
+ repo: z.string().describe("Repository identifier"),
212
+ module: z.string().optional().describe("Filter by module/area"),
213
+ timeframe: z
214
+ .string()
215
+ .optional()
216
+ .describe("Time filter (e.g., '7d', '24h')"),
217
+ }, async (params) => {
218
+ const result = await client.query({
219
+ event_type: "decision",
220
+ module: params.module,
221
+ timeframe: params.timeframe,
222
+ });
223
+ const text = formatEvents(result.results);
224
+ return {
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: `## Decision log${params.module ? ` for "${params.module}"` : ""}\n\n${text}`,
229
+ },
230
+ ],
231
+ };
232
+ });
233
+ // ─── patterns ──────────────────────────────────────────────────
234
+ server.tool("patterns", "Get recurring insights and patterns identified across agent sessions", {
235
+ repo: z.string().describe("Repository identifier"),
236
+ }, async (params) => {
237
+ try {
238
+ const result = await client.getLearnings({
239
+ repo: params.repo,
240
+ min_confidence: 0.5,
241
+ sort_by: "confidence",
242
+ sort_order: "desc",
243
+ });
244
+ const learnings = result.learnings || [];
245
+ if (learnings.length === 0) {
246
+ return {
247
+ content: [
248
+ {
249
+ type: "text",
250
+ text: "No patterns identified yet. Patterns emerge after consolidation runs process reasoning events.",
251
+ },
252
+ ],
253
+ };
254
+ }
255
+ const text = learnings
256
+ .map((l) => `[confidence: ${(l.confidence ?? 0).toFixed(2)}] ${l.summary}\n tags: ${(l.tags ?? []).join(", ") || "none"}\n files: ${(l.file_patterns ?? []).join(", ") || "none"}`)
257
+ .join("\n\n");
258
+ return {
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: `## Patterns for ${params.repo}\n\n${text}`,
263
+ },
264
+ ],
265
+ };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ content: [{ type: "text", text: `Error: ${error}` }],
270
+ isError: true,
271
+ };
272
+ }
273
+ });
274
+ // ─── suggest ───────────────────────────────────────────────────
275
+ server.tool("suggest", "Get ranked task priorities based on accumulated reasoning", {
276
+ repo: z.string().describe("Repository identifier"),
277
+ backlog: z
278
+ .array(z.string())
279
+ .min(1)
280
+ .describe("List of backlog items to prioritize"),
281
+ }, async (params) => {
282
+ try {
283
+ // Fetch learnings to rank backlog items by relevance to institutional knowledge
284
+ const learningsResult = await client.getLearnings({
285
+ repo: params.repo,
286
+ sort_by: "confidence",
287
+ sort_order: "desc",
288
+ });
289
+ const learnings = learningsResult.learnings || [];
290
+ // Score each backlog item by how many learnings mention related terms
291
+ const scored = params.backlog.map((task) => {
292
+ const taskLower = task.toLowerCase();
293
+ const keywords = taskLower.split(/\s+/).filter((w) => w.length > 3);
294
+ let score = 0;
295
+ const reasons = [];
296
+ for (const l of learnings) {
297
+ const summary = (l.summary || "").toLowerCase();
298
+ const tags = (l.tags || []).join(" ").toLowerCase();
299
+ const matched = keywords.filter((kw) => summary.includes(kw) || tags.includes(kw));
300
+ if (matched.length > 0) {
301
+ score += (l.confidence ?? 0.5) * matched.length;
302
+ reasons.push(`${((l.confidence ?? 0) * 100).toFixed(0)}% confidence learning: "${(l.summary || "").slice(0, 80)}..."`);
303
+ }
304
+ }
305
+ return { task, score: Math.round(score * 100) / 100, reasons };
306
+ });
307
+ scored.sort((a, b) => b.score - a.score);
308
+ const text = scored
309
+ .map((item, i) => `${i + 1}. **${item.task}** (evidence score: ${item.score})\n` +
310
+ (item.reasons.length > 0
311
+ ? item.reasons
312
+ .slice(0, 3)
313
+ .map((r) => ` - ${r}`)
314
+ .join("\n")
315
+ : ` _No matching learnings yet_`))
316
+ .join("\n\n");
317
+ return {
318
+ content: [
319
+ {
320
+ type: "text",
321
+ text: `## Suggested priorities\n\n${text}`,
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ catch (error) {
327
+ return {
328
+ content: [{ type: "text", text: `Error: ${error}` }],
329
+ isError: true,
330
+ };
331
+ }
332
+ });
333
+ // ─── backlog ───────────────────────────────────────────────────
334
+ server.tool("backlog", "Get the auto-generated agentic product backlog. " +
335
+ "PBIs are generated by the consolidation engine from high-confidence learnings. " +
336
+ "Each PBI includes evidence summary, suggested approach, and full context.", {
337
+ repo: z.string().optional().describe("Repository to scope to"),
338
+ status: z
339
+ .string()
340
+ .optional()
341
+ .describe("Filter by status: draft, approved, assigned, in_progress, done, rejected"),
342
+ priority: z
343
+ .string()
344
+ .optional()
345
+ .describe("Filter by priority: CRITICAL, HIGH, MEDIUM, LOW"),
346
+ tags: z.array(z.string()).optional().describe("Filter by tags"),
347
+ max_results: z.number().optional().default(20),
348
+ }, async (params) => {
349
+ try {
350
+ const backlog = await client.getBacklog({
351
+ repo: params.repo,
352
+ status: params.status,
353
+ priority: params.priority,
354
+ tags: params.tags?.join(","),
355
+ limit: params.max_results,
356
+ });
357
+ if (!backlog || backlog.length === 0) {
358
+ return {
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: "Backlog is empty. PBIs are auto-generated when the consolidation engine " +
363
+ "runs and identifies actionable insights from reasoning events.",
364
+ },
365
+ ],
366
+ };
367
+ }
368
+ const lines = [
369
+ `## Agentic Product Backlog`,
370
+ ``,
371
+ `> ${backlog.length} item(s) | Auto-generated from consolidated learnings`,
372
+ ``,
373
+ ];
374
+ for (const item of backlog) {
375
+ lines.push(`### [${item.priority}] ${item.title}`);
376
+ lines.push(`**Status:** ${item.status} | **Scope:** ${item.scope}`);
377
+ if (item.description)
378
+ lines.push(item.description);
379
+ if (item.evidence_summary)
380
+ lines.push(`**Evidence:** ${item.evidence_summary}`);
381
+ if (item.suggested_approach)
382
+ lines.push(`**Approach:** ${item.suggested_approach}`);
383
+ lines.push("");
384
+ }
385
+ return {
386
+ content: [{ type: "text", text: lines.join("\n") }],
387
+ };
388
+ }
389
+ catch (error) {
390
+ return {
391
+ content: [
392
+ {
393
+ type: "text",
394
+ text: `Failed to retrieve backlog: ${error}`,
395
+ },
396
+ ],
397
+ isError: true,
398
+ };
399
+ }
400
+ });
401
+ // ─── priorities ────────────────────────────────────────────────
402
+ server.tool("priorities", "Get evidence-ranked priorities for what to build next. " +
403
+ "Priorities are derived from consolidated learnings across agent sessions, " +
404
+ "code changes, meetings, tickets, and chat.", {
405
+ repo: z.string().optional().describe("Repository to scope priorities to"),
406
+ current_tasks: z
407
+ .array(z.string())
408
+ .optional()
409
+ .describe("Current task descriptions for context-aware ranking"),
410
+ max_results: z
411
+ .number()
412
+ .optional()
413
+ .default(10)
414
+ .describe("Maximum number of priorities to return"),
415
+ }, async (params) => {
416
+ try {
417
+ const backlog = await client.getBacklog({
418
+ repo: params.repo,
419
+ status: "approved",
420
+ limit: params.max_results,
421
+ });
422
+ if (!backlog || backlog.length === 0) {
423
+ return {
424
+ content: [
425
+ {
426
+ type: "text",
427
+ text: "No approved priorities in the backlog. " +
428
+ "Run consolidation to generate PBIs from accumulated reasoning, " +
429
+ "then have a PM approve them for work.",
430
+ },
431
+ ],
432
+ };
433
+ }
434
+ const lines = [
435
+ "## Evidence-Ranked Priorities",
436
+ "",
437
+ "> Derived from consolidated institutional knowledge, not guesswork.",
438
+ "",
439
+ ];
440
+ for (let i = 0; i < backlog.length; i++) {
441
+ const item = backlog[i];
442
+ lines.push(`${i + 1}. **[${item.priority}] ${item.title}**`);
443
+ lines.push(` Status: ${item.status} | Scope: ${item.scope}`);
444
+ if (item.evidence_summary) {
445
+ lines.push(` Evidence: ${item.evidence_summary}`);
446
+ }
447
+ lines.push("");
448
+ }
449
+ return {
450
+ content: [{ type: "text", text: lines.join("\n") }],
451
+ };
452
+ }
453
+ catch (error) {
454
+ return {
455
+ content: [
456
+ {
457
+ type: "text",
458
+ text: `Failed to retrieve priorities: ${error}`,
459
+ },
460
+ ],
461
+ isError: true,
462
+ };
463
+ }
464
+ });
465
+ // ─── semantic_search ─────────────────────────────────────────
466
+ server.tool("semantic_search", "Search institutional knowledge using natural language. " +
467
+ "Uses vector embeddings for semantic similarity — finds learnings " +
468
+ "that are conceptually related to your query even if they don't share " +
469
+ "exact keywords. Requires the embedding provider to be configured.", {
470
+ query: z
471
+ .string()
472
+ .describe("Natural language query (e.g., 'authentication token refresh')"),
473
+ repo: z
474
+ .string()
475
+ .optional()
476
+ .describe("Repository identifier. Optional for account_wide (cross-repo) search."),
477
+ limit: z.number().optional().default(10).describe("Max results"),
478
+ min_similarity: z
479
+ .number()
480
+ .optional()
481
+ .default(0.7)
482
+ .describe("Minimum cosine similarity (0.0–1.0)"),
483
+ visibility: z
484
+ .enum(["user_repo", "account_wide", "user_private"])
485
+ .optional()
486
+ .describe("Visibility scope: user_repo (default, repo-scoped), " +
487
+ "account_wide (cross-repo within account), " +
488
+ "user_private (personal learnings only)"),
489
+ }, async (params) => {
490
+ try {
491
+ const result = await client.semanticSearch(params.query, {
492
+ repo: params.repo,
493
+ limit: params.limit,
494
+ min_similarity: params.min_similarity,
495
+ visibility: params.visibility,
496
+ });
497
+ if (result.results.length === 0) {
498
+ return {
499
+ content: [
500
+ {
501
+ type: "text",
502
+ text: `No semantically similar learnings found for "${params.query}". ` +
503
+ "The embedding provider may not be configured, or no learnings have been embedded yet.",
504
+ },
505
+ ],
506
+ };
507
+ }
508
+ const lines = [
509
+ `## Semantic Search: ${params.query}`,
510
+ "",
511
+ `> ${result.total} result(s) found.`,
512
+ "",
513
+ ];
514
+ for (const r of result.results) {
515
+ const sim = (r.similarity * 100).toFixed(0);
516
+ lines.push(`- **[${sim}%]** ${r.summary}`);
517
+ lines.push(` *ID: ${r.id}*`);
518
+ lines.push("");
519
+ }
520
+ return {
521
+ content: [{ type: "text", text: lines.join("\n") }],
522
+ };
523
+ }
524
+ catch (error) {
525
+ return {
526
+ content: [
527
+ {
528
+ type: "text",
529
+ text: `Semantic search failed: ${error}`,
530
+ },
531
+ ],
532
+ isError: true,
533
+ };
534
+ }
535
+ });
536
+ // ─── consolidate ──────────────────────────────────────────────
537
+ server.tool("consolidate", "Trigger a consolidation run to process recent reasoning events " +
538
+ "and derive new learnings and backlog items. Use this when you want " +
539
+ "to refresh the institutional knowledge base.", {
540
+ repo: z.string().describe("Repository to run consolidation for"),
541
+ max_events: z.number().optional().describe("Maximum events to process"),
542
+ }, async (params) => {
543
+ try {
544
+ const result = await client.triggerConsolidation({
545
+ repo: params.repo,
546
+ trigger: "manual",
547
+ max_events: params.max_events,
548
+ });
549
+ const lines = [
550
+ "## Consolidation Complete",
551
+ "",
552
+ `**Run ID:** ${result.run_id}`,
553
+ `**Events processed:** ${result.events_processed}`,
554
+ `**New learnings:** ${result.new_learnings}`,
555
+ `**Reinforced:** ${result.reinforced}`,
556
+ `**Contradicted:** ${result.contradicted}`,
557
+ `**Backlog items generated:** ${result.backlog_items_generated}`,
558
+ ];
559
+ if (result.model_id) {
560
+ lines.push(`**Model:** ${result.model_id}`);
561
+ }
562
+ if (result.input_tokens) {
563
+ lines.push(`**Tokens:** ${result.input_tokens} in / ${result.output_tokens} out`);
564
+ }
565
+ return {
566
+ content: [{ type: "text", text: lines.join("\n") }],
567
+ };
568
+ }
569
+ catch (error) {
570
+ return {
571
+ content: [
572
+ { type: "text", text: `Consolidation failed: ${error}` },
573
+ ],
574
+ isError: true,
575
+ };
576
+ }
577
+ });
578
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OkaClient } from "../client.js";
3
+ export declare function registerEnhancedReadTools(server: McpServer, client: OkaClient): void;
4
+ //# sourceMappingURL=read_enhanced.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read_enhanced.d.ts","sourceRoot":"","sources":["../../src/tools/read_enhanced.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAoE9C,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,SAAS,GAChB,IAAI,CAqPN"}