mio-agent-runtime 0.5.15 → 0.5.17

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.
@@ -5,11 +5,79 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const crypto = require('crypto')
7
7
  const readline = require('readline')
8
- const { execFileSync } = require('child_process')
8
+ const { execFileSync } = require('child_process')
9
+ const { getEvolutionStatus } = require('../runtime-modules.js')
10
+ const { listHostCapabilities } = require('../host-capabilities.js')
11
+ const { createEvolutionCutoverTools } = require('../evolution-cutover.js')
12
+
13
+ const { CreativityEngine } = require('../creativity-engine.js')
14
+
15
+ // Observer research pipeline (optional — installed via @akemi-mio/observer)
16
+ let ObserverStore = null
17
+ let ObserverService = null
18
+ try {
19
+ const obs = require('@akemi-mio/observer')
20
+ ObserverStore = obs.ObserverStore
21
+ ObserverService = obs.ObserverService
22
+ } catch {}
23
+
24
+ // Insight self-observation engine (optional — installed via @akemi-mio/insight)
25
+ let InsightStore = null
26
+ let InsightGenerator = null
27
+ let PresenceService = null
28
+ try {
29
+ const insight = require('@akemi-mio/insight')
30
+ InsightStore = insight.InsightStore
31
+ InsightGenerator = insight.InsightGenerator
32
+ PresenceService = insight.PresenceService
33
+ } catch {}
9
34
 
10
35
  const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
11
36
  const PROTOCOL_VERSION = '2024-11-05'
12
37
 
38
+ // ═══════════════════════════════════════════════
39
+ // LLM chatJson — used by creativity engine
40
+ // ═══════════════════════════════════════════════
41
+
42
+ async function chatJson(userText, opts = {}) {
43
+ const apiUrl = process.env.LLM_API_URL || 'https://opencode.ai/zen/go/v1/chat/completions'
44
+ const apiKey = process.env.LLM_KEY || ''
45
+ const model = process.env.LLM_CHAT_MODEL || process.env.LLM_MODEL || 'deepseek-v4-flash'
46
+ const system = opts.system || 'You are a creative AI assistant. Output JSON.'
47
+ const temperature = opts.temperature ?? 0.3
48
+
49
+ try {
50
+ const resp = await fetch(apiUrl, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
55
+ },
56
+ body: JSON.stringify({
57
+ model,
58
+ temperature,
59
+ messages: [
60
+ { role: 'system', content: system },
61
+ { role: 'user', content: userText },
62
+ ],
63
+ response_format: { type: 'json_object' },
64
+ }),
65
+ signal: AbortSignal.timeout(opts.timeoutMs || 30000),
66
+ })
67
+
68
+ if (!resp.ok) return { error: `HTTP ${resp.status}` }
69
+ const json = await resp.json()
70
+ const text = json.choices?.[0]?.message?.content || ''
71
+ try {
72
+ return { data: JSON.parse(text) }
73
+ } catch {
74
+ return { data: text }
75
+ }
76
+ } catch (err) {
77
+ return { error: String(err) }
78
+ }
79
+ }
80
+
13
81
  const dataDir = path.resolve(
14
82
  process.env.MIO_DATA_DIR || path.join(process.cwd(), '.mio-intelligence')
15
83
  )
@@ -20,11 +88,12 @@ const queryPath = path.join(dataDir, 'queries.jsonl')
20
88
  const agentsPath = path.join(dataDir, 'agents.jsonl')
21
89
  const subscriptionPath = path.join(dataDir, 'subscriptions.jsonl')
22
90
  const digestStatePath = path.join(dataDir, 'digest_state.json')
23
- const ideasPath = path.join(dataDir, 'ideas.jsonl')
24
- const suggestionsPath = path.join(dataDir, 'suggestions.jsonl')
25
- fs.mkdirSync(dataDir, { recursive: true })
26
-
91
+ fs.mkdirSync(dataDir, { recursive: true })
92
+
27
93
  const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
94
+ const evolutionCutover = createEvolutionCutoverTools({ dataDir, appendJsonl, readJsonl, projectName })
95
+ const creativityEngine = new CreativityEngine(path.join(dataDir, 'creativity'), chatJson)
96
+ const insightStore = InsightStore ? new InsightStore(path.join(dataDir, 'insights', 'insights.json')) : null
28
97
 
29
98
  let runtimeContext = {}
30
99
  try {
@@ -1594,209 +1663,534 @@ const TOOLS = [
1594
1663
  },
1595
1664
  },
1596
1665
  },
1597
- {
1598
- name: 'mio.evolution.report',
1599
- description: 'Generate cross-agent evolution report: ecosystem summary, agent performance comparison, cross-agent patterns, memory health, and optimization suggestions (ADR-017 Evolution plane).',
1666
+ {
1667
+ name: 'mio.evolution.report',
1668
+ description: 'Generate cross-agent evolution report: ecosystem summary, agent performance comparison, cross-agent patterns, memory health, and optimization suggestions (ADR-017 Evolution plane).',
1600
1669
  inputSchema: {
1601
1670
  type: 'object',
1602
1671
  properties: {
1603
1672
  project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1604
1673
  period: { type: 'string', description: 'Time window: 24h, 7d, 30d, or all (default).' },
1605
1674
  },
1606
- },
1607
- },
1608
- {
1609
- name: 'mio.task.record_outcome',
1610
- description: 'Record task outcome with agent registry update and auto-claim experience reuse in one atomic call. Combines observer.ingest + agent stats + auto-claim for a complete task lifecycle closure.',
1611
- inputSchema: {
1612
- type: 'object',
1613
- properties: {
1614
- outcome: { type: 'string', description: 'Task outcome: success, failure, or aborted.' },
1615
- task: { type: 'string', description: 'Brief task description.' },
1616
- summary: { type: 'string', description: 'Optional task summary.' },
1617
- verification: { type: 'string', description: 'Optional verification details.' },
1618
- agentId: { type: 'string', description: 'Agent identifier. Defaults to MIO_CONTEXT agentId.' },
1619
- project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1620
- traceId: { type: 'string', description: 'Optional custom trace ID. Auto-generated if omitted.' },
1621
- },
1622
- required: ['outcome'],
1623
- },
1675
+ },
1624
1676
  },
1625
1677
  {
1626
- name: 'mio.idea.generate',
1627
- description: 'Generate creative ideas or multiple candidate solutions based on a given goal, context, and constraints.',
1678
+ name: 'mio.evolution.status',
1679
+ description: 'Report composed mio-agent-runtime module health from the canonical runtime module registry.',
1680
+ inputSchema: {
1681
+ type: 'object',
1682
+ properties: {},
1683
+ },
1684
+ },
1685
+ {
1686
+ name: 'mio.host.capabilities',
1687
+ description: 'List installed desktop host adapters and the safe capability surface they expose to runtime modules.',
1688
+ inputSchema: {
1689
+ type: 'object',
1690
+ properties: {},
1691
+ },
1692
+ },
1693
+ {
1694
+ name: 'mio.evolution.shadow.record',
1695
+ description: 'Record a shadow comparison between legacy and modular runtime outputs without changing authority.',
1628
1696
  inputSchema: {
1629
1697
  type: 'object',
1630
- required: ['goal'],
1631
1698
  properties: {
1632
- goal: { type: 'string', description: 'The goal or problem description for which to generate ideas.' },
1633
- context: { type: 'string', description: '(Optional) Additional background information, such as prior decisions or known problems.' },
1634
- constraints: { type: 'array', items: { type: 'string' }, description: '(Optional) List of constraints, e.g. "no new dependencies", "must be implementable as a plugin".' },
1635
- numIdeas: { type: 'integer', description: '(Optional) Number of ideas to return, default 3.', default: 3, minimum: 1 }
1699
+ label: { type: 'string', description: 'Human-readable comparison label.' },
1700
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1701
+ legacy: { type: 'object', description: 'Legacy path output snapshot.' },
1702
+ modular: { type: 'object', description: 'Modular path output snapshot.' },
1703
+ input: { type: 'object', description: 'Optional sanitized input snapshot.' },
1636
1704
  },
1637
- additionalProperties: false
1705
+ required: ['legacy', 'modular'],
1638
1706
  },
1639
1707
  },
1640
1708
  {
1641
- name: 'mio.idea.list',
1642
- description: 'List previously generated ideas, optionally filtered by keyword, strategy, or time window.',
1709
+ name: 'mio.evolution.dual_write.record',
1710
+ description: 'Record a dual-write sample result for legacy and modular state paths without switching authority.',
1643
1711
  inputSchema: {
1644
1712
  type: 'object',
1645
1713
  properties: {
1646
- goal: { type: 'string', description: '(Optional) Filter ideas whose goal contains this keyword.' },
1647
- strategy: { type: 'string', description: '(Optional) Filter by generation strategy (e.g. SCAMPER, Analogy).' },
1648
- limit: { type: 'integer', description: '(Optional) Max results, default 10.', default: 10, minimum: 1 },
1649
- period: { type: 'string', description: '(Optional) Time window: 24h, 7d, 30d, or all (default).' }
1650
- }
1714
+ label: { type: 'string', description: 'Human-readable dual-write label.' },
1715
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1716
+ authoritative: { type: 'string', description: 'Authoritative path: legacy or modular. Defaults to legacy.' },
1717
+ legacyResult: { type: 'object', description: 'Legacy write result snapshot.' },
1718
+ modularResult: { type: 'object', description: 'Modular write result snapshot.' },
1719
+ record: { type: 'object', description: 'Optional sanitized record metadata.' },
1720
+ },
1721
+ required: ['legacyResult', 'modularResult'],
1651
1722
  },
1652
1723
  },
1653
1724
  {
1654
- name: 'mio.evolution.suggest',
1655
- description: 'Analyze ecosystem data and produce prioritized improvement suggestions with next actions and expected impact.',
1725
+ name: 'mio.evolution.cutover.readiness',
1726
+ description: 'Assess whether shadow and dual-write samples are sufficient to consider modular runtime cutover.',
1656
1727
  inputSchema: {
1657
1728
  type: 'object',
1658
1729
  properties: {
1659
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' }
1660
- }
1730
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1731
+ minShadowRuns: { type: 'number', description: 'Minimum shadow comparison samples required. Defaults to 5.' },
1732
+ maxMismatchRate: { type: 'number', description: 'Allowed shadow mismatch rate. Defaults to 0.' },
1733
+ },
1661
1734
  },
1662
1735
  },
1663
- ,
1664
1736
  {
1665
- name: 'mio.dashboard.start',
1666
- description: 'Start the Mio live dashboard HTTP server and return its URL. Aggregates MIO_HOME data into a browser-viewable dashboard.',
1737
+ name: 'mio.evolution.migration.plan',
1738
+ description: 'Preview a state migration plan between legacy and modular records without writing state.',
1667
1739
  inputSchema: {
1668
1740
  type: 'object',
1669
1741
  properties: {
1670
- port: { type: 'integer', description: '(Optional) Port to bind, default 8787.', default: 8787, minimum: 1 },
1671
- open: { type: 'boolean', description: '(Optional) Open the dashboard in the default browser after starting.' }
1672
- }
1742
+ legacyRecords: { type: 'array', items: { type: 'object' }, description: 'Legacy state records.' },
1743
+ modularRecords: { type: 'array', items: { type: 'object' }, description: 'Existing modular state records.' },
1744
+ },
1673
1745
  },
1674
1746
  },
1675
1747
  {
1676
- name: 'mio.dashboard.stop',
1677
- description: 'Stop the running Mio dashboard HTTP server.',
1678
- inputSchema: { type: 'object', properties: {} },
1748
+ name: 'mio.evolution.authority.plan',
1749
+ description: 'Create a gated authority switch plan from legacy to modular runtime without applying it.',
1750
+ inputSchema: {
1751
+ type: 'object',
1752
+ properties: {
1753
+ readiness: { type: 'object', description: 'Result from mio.evolution.cutover.readiness.' },
1754
+ from: { type: 'string', description: 'Current authoritative path. Defaults to legacy.' },
1755
+ to: { type: 'string', description: 'Target authoritative path. Defaults to modular.' },
1756
+ },
1757
+ required: ['readiness'],
1758
+ },
1679
1759
  },
1680
1760
  {
1681
- name: 'mio.dashboard.status',
1682
- description: 'Check if the Mio dashboard HTTP server is running.',
1683
- inputSchema: { type: 'object', properties: {} },
1684
- }]
1685
-
1686
- function handleIdeaGenerate(args = {}) {
1687
- const goal = String(args.goal || '').trim()
1688
- if (!goal) throw new Error('idea.generate requires a goal')
1689
- const numIdeas = Math.min(Math.max(Number(args.numIdeas) || 3, 1), 10)
1690
- const context = args.context ? String(args.context).trim() : ''
1691
- const constraints = Array.isArray(args.constraints) ? args.constraints.map(String) : []
1692
-
1693
- // Query Mio memory for related context
1694
- const relatedRecords = readJsonl(memoryPath)
1695
- .filter(r => r.archived !== true)
1696
- .filter(r => {
1697
- const text = (r.content || '').toLowerCase()
1698
- const goalLower = goal.toLowerCase()
1699
- // Support both space-separated (English) and continuous (Chinese) matching
1700
- const goalWords = goalLower.split(/\s+/).filter(w => w.length > 2)
1701
- // For Chinese: extract 2-char n-grams as search tokens
1702
- const ngrams = []
1703
- for (let i = 0; i < goalLower.length - 1; i++) {
1704
- const ng = goalLower.slice(i, i + 2)
1705
- if (ng.trim().length === 2) ngrams.push(ng)
1706
- }
1707
- const allTokens = [...new Set([...goalWords, ...ngrams])]
1708
- return allTokens.some(t => text.includes(t))
1709
- })
1710
- .slice(0, 8)
1711
-
1712
- const relatedMemoryText = relatedRecords.length > 0
1713
- ? relatedRecords.map(r => '- [' + r.kind + '] ' + (r.content || '').slice(0, 200)).join('\n')
1714
- : '(no related memories found)'
1715
-
1716
- const strategies = [
1717
- {
1718
- name: 'SCAMPER',
1719
- prompt: 'Apply SCAMPER: Substitute, Combine, Adapt, Modify, Put to another use, Eliminate, Reverse. ' +
1720
- 'For goal: ' + goal + '. What novel combination or modification of existing elements could work?'
1721
- },
1722
- {
1723
- name: 'Analogy',
1724
- prompt: 'By analogy: What similar problem has been solved in a completely different domain? ' +
1725
- 'For goal: ' + goal + '. What approach from another field (biology, economics, gaming, logistics) transfers here?'
1726
- },
1727
- {
1728
- name: 'First-Principles',
1729
- prompt: 'First principles: Strip away all assumptions. What are the absolute basics of ' + goal + '? ' +
1730
- 'What would you build if you had no prior art, no constraints, no legacy?'
1731
- },
1732
- {
1733
- name: 'Random-Stimulus',
1734
- prompt: 'Random stimulus: Pick an unrelated concept (a relay race, a restaurant menu, a river delta, a beehive). ' +
1735
- 'How does it relate to ' + goal + '? What structural or procedural parallel exists?'
1736
- },
1737
- {
1738
- name: 'Constraint-Inversion',
1739
- prompt: 'Constraint inversion: Instead of ' + goal + ', what is the OPPOSITE goal? ' +
1740
- 'How would you deliberately make things worse? Now invert the inversion — what does that reveal about the real solution?'
1741
- }
1742
- ]
1743
-
1744
- const selectedStrategies = strategies.slice(0, numIdeas)
1745
- const ideas = []
1746
-
1747
- for (let i = 0; i < selectedStrategies.length; i++) {
1748
- const s = selectedStrategies[i]
1749
- const idea = {
1750
- id: createId('idea'),
1751
- title: 'Idea #' + (i + 1) + ' (' + s.name + '): ' + goal,
1752
- description: 'Goal: ' + goal + '\n\n' +
1753
- 'Strategy: ' + s.name + '\n\n' +
1754
- 'Related Mio memory:\n' + relatedMemoryText + '\n\n' +
1755
- (context ? 'Context: ' + context + '\n\n' : '') +
1756
- (constraints.length > 0 ? 'Constraints:\n' + constraints.map(c => '- ' + c).join('\n') + '\n\n' : '') +
1757
- 'Strategy prompt: ' + s.prompt + '\n\n' +
1758
- 'Suggestion: Consider prototyping the idea and measuring its impact against prior decisions.',
1759
- provenance: {
1760
- strategy: s.name,
1761
- relatedMemoryIds: relatedRecords.map(r => r.id).slice(0, 5),
1761
+ name: 'mio.evolution.cutover.apply',
1762
+ description: 'Dry-run an authority switch plan. This stage refuses real cutover and never mutates authority state.',
1763
+ inputSchema: {
1764
+ type: 'object',
1765
+ properties: {
1766
+ dryRun: { type: 'boolean', description: 'Must be true. Real cutover is intentionally gated.' },
1767
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1768
+ plan: { type: 'object', description: 'Authority switch plan from mio.evolution.authority.plan.' },
1762
1769
  },
1763
- generatedAt: new Date().toISOString(),
1764
- goal,
1765
- context: context || null,
1766
- constraints,
1767
- }
1768
- ideas.push(idea)
1769
- }
1770
-
1771
- // Persist to ideas.jsonl
1772
- for (const idea of ideas) {
1773
- appendJsonl(ideasPath, idea)
1774
- }
1775
-
1776
- return { ideas, count: ideas.length, goal, strategies: ideas.map(i => i.provenance.strategy) }
1777
- }
1778
-
1779
- function listIdeas(args = {}) {
1780
- if (!fs.existsSync(ideasPath)) return { ideas: [], count: 0, total: 0 }
1781
- const now = Date.now()
1782
- const period = String(args.period || 'all').trim()
1783
- const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : period === '30d' ? 2592000000 : Infinity
1784
- const cutoff = periodMs === Infinity ? 0 : now - periodMs
1785
- const goalFilter = args.goal ? String(args.goal).toLowerCase() : null
1786
- const strategyFilter = args.strategy ? String(args.strategy).trim().toLowerCase() : null
1787
- const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50)
1788
- const all = fs.readFileSync(ideasPath, 'utf8').split('\n').filter(Boolean).map(line => {
1789
- try { return JSON.parse(line) } catch (_) { return null }
1790
- }).filter(Boolean)
1791
- const filtered = all.filter(idea => {
1792
- if (cutoff && new Date(idea.generatedAt).getTime() < cutoff) return false
1793
- if (goalFilter && !String(idea.goal || '').toLowerCase().includes(goalFilter)) return false
1794
- if (strategyFilter && String(idea.provenance?.strategy || '').toLowerCase() !== strategyFilter) return false
1795
- return true
1796
- })
1797
- return { ideas: filtered.slice(-limit), count: filtered.length, total: all.length }
1798
- }
1799
-
1770
+ required: ['dryRun', 'plan'],
1771
+ },
1772
+ },
1773
+ {
1774
+ name: 'mio.task.record_outcome',
1775
+ description: 'Record task outcome with agent registry update and auto-claim experience reuse in one atomic call. Combines observer.ingest + agent stats + auto-claim for a complete task lifecycle closure.',
1776
+ inputSchema: {
1777
+ type: 'object',
1778
+ properties: {
1779
+ outcome: { type: 'string', description: 'Task outcome: success, failure, or aborted.' },
1780
+ task: { type: 'string', description: 'Brief task description.' },
1781
+ summary: { type: 'string', description: 'Optional task summary.' },
1782
+ verification: { type: 'string', description: 'Optional verification details.' },
1783
+ agentId: { type: 'string', description: 'Agent identifier. Defaults to MIO_CONTEXT agentId.' },
1784
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1785
+ traceId: { type: 'string', description: 'Optional custom trace ID. Auto-generated if omitted.' },
1786
+ },
1787
+ required: ['outcome'],
1788
+ },
1789
+ },
1790
+ {
1791
+ name: 'mio.creativity.status',
1792
+ description: 'Show creativity engine status: hypothesis counts, active/validated/rejected, recent top ideas.',
1793
+ inputSchema: { type: 'object', properties: {} },
1794
+ },
1795
+ {
1796
+ name: 'mio.creativity.list',
1797
+ description: 'List creativity hypotheses with optional status filter.',
1798
+ inputSchema: {
1799
+ type: 'object',
1800
+ properties: {
1801
+ status: { type: 'string', description: 'Filter by status: active, validated, rejected, draft.' },
1802
+ limit: { type: 'number', description: 'Max results. Default 20.' },
1803
+ },
1804
+ },
1805
+ },
1806
+ {
1807
+ name: 'mio.creativity.generate',
1808
+ description: 'Generate new creative hypotheses by combining concept sources via LLM. Returns ideas with novelty/feasibility/impact scores.',
1809
+ inputSchema: {
1810
+ type: 'object',
1811
+ properties: {
1812
+ sources: {
1813
+ type: 'array',
1814
+ description: 'Concept sources to combine. Each has name, content, type (knowledge/behavior/insight/failure), weight.',
1815
+ items: {
1816
+ type: 'object',
1817
+ properties: {
1818
+ name: { type: 'string' },
1819
+ content: { type: 'string' },
1820
+ type: { type: 'string' },
1821
+ weight: { type: 'number' },
1822
+ },
1823
+ required: ['name', 'content'],
1824
+ },
1825
+ },
1826
+ strategy: { type: 'string', description: 'Generation strategy: explore (cross-domain), signal (provocative), stable (conservative). Auto-selected if omitted.' },
1827
+ },
1828
+ required: ['sources'],
1829
+ },
1830
+ },
1831
+ {
1832
+ name: 'mio.creativity.ferment',
1833
+ description: 'Ferment (review and refine) active hypotheses. Updates scores, can promote/reject ideas.',
1834
+ inputSchema: {
1835
+ type: 'object',
1836
+ properties: {
1837
+ limit: { type: 'number', description: 'Max hypotheses to ferment. Default 5.' },
1838
+ },
1839
+ },
1840
+ },
1841
+
1842
+ // ═══ Observer Research Pipeline ═══
1843
+
1844
+ // ═══ Insight Self-Observation ═══
1845
+
1846
+ ...(insightStore ? [{
1847
+ name: 'mio.insight.status',
1848
+ description: 'Get insight engine status: total insights, unreported count, cooldown.',
1849
+ inputSchema: { type: 'object', properties: {} },
1850
+ },
1851
+ {
1852
+ name: 'mio.insight.list',
1853
+ description: 'List stored insights with optional filters.',
1854
+ inputSchema: {
1855
+ type: 'object',
1856
+ properties: {
1857
+ unreported: { type: 'boolean', description: 'Only unreported insights' },
1858
+ minScore: { type: 'number', description: 'Minimum score filter' },
1859
+ detector: { type: 'string', description: 'Filter by detector name' },
1860
+ limit: { type: 'number', description: 'Max results (default 20)' },
1861
+ },
1862
+ },
1863
+ },
1864
+ {
1865
+ name: 'mio.insight.mark_reported',
1866
+ description: 'Mark insights as reported (acknowledged).',
1867
+ inputSchema: {
1868
+ type: 'object',
1869
+ properties: {
1870
+ ids: { type: 'array', items: { type: 'string' }, description: 'Insight IDs to mark' },
1871
+ },
1872
+ required: ['ids'],
1873
+ },
1874
+ },
1875
+ {
1876
+ name: 'mio.insight.generate',
1877
+ description: 'Run LLM-based insight generation from provided context data.',
1878
+ inputSchema: {
1879
+ type: 'object',
1880
+ properties: {
1881
+ memories: { type: 'array', items: { type: 'object' }, description: 'Memory entries to analyze' },
1882
+ summaries: { type: 'array', items: { type: 'string' }, description: 'Recent summaries' },
1883
+ plans: { type: 'array', items: { type: 'object' }, description: 'Active plans' },
1884
+ },
1885
+ },
1886
+ }] : []),
1887
+
1888
+ // ═══ Observer Research Pipeline ═══
1889
+
1890
+ ...(ObserverStore ? [{
1891
+ name: 'mio.observer.world_model',
1892
+ description: 'Query the Observer world model: entities, events, trends, narratives, uncertainties, and relations.',
1893
+ inputSchema: {
1894
+ type: 'object',
1895
+ properties: {
1896
+ baseDir: { type: 'string', description: 'Observer data directory (default: .local/observer)' },
1897
+ },
1898
+ },
1899
+ },
1900
+ {
1901
+ name: 'mio.observer.trends',
1902
+ description: 'Get recent trend reports from the Observer pipeline.',
1903
+ inputSchema: {
1904
+ type: 'object',
1905
+ properties: {
1906
+ date: { type: 'string', description: 'Specific date (YYYY-MM-DD) or null for latest' },
1907
+ limit: { type: 'number', description: 'Max reports to return (default 7)' },
1908
+ baseDir: { type: 'string', description: 'Observer data directory' },
1909
+ },
1910
+ },
1911
+ },
1912
+ {
1913
+ name: 'mio.observer.research',
1914
+ description: 'List recent deep research results from the Observer pipeline.',
1915
+ inputSchema: {
1916
+ type: 'object',
1917
+ properties: {
1918
+ limit: { type: 'number', description: 'Max results (default 10)' },
1919
+ baseDir: { type: 'string', description: 'Observer data directory' },
1920
+ },
1921
+ },
1922
+ },
1923
+ {
1924
+ name: 'mio.observer.insights',
1925
+ description: 'List Observer-generated insights (5-section structured articles).',
1926
+ inputSchema: {
1927
+ type: 'object',
1928
+ properties: {
1929
+ limit: { type: 'number', description: 'Max insights (default 20)' },
1930
+ baseDir: { type: 'string', description: 'Observer data directory' },
1931
+ },
1932
+ },
1933
+ },
1934
+ {
1935
+ name: 'mio.observer.status',
1936
+ description: 'Get Observer pipeline status: counts of observations, trends, topics, research, insights, essays, and world model entities.',
1937
+ inputSchema: {
1938
+ type: 'object',
1939
+ properties: {
1940
+ baseDir: { type: 'string', description: 'Observer data directory' },
1941
+ },
1942
+ },
1943
+ },
1944
+ {
1945
+ name: 'mio.observer.collect',
1946
+ description: 'Force data collection from all configured sources (RSS, Bilibili, HackerNews, GitHub, Douyin). Returns collected observations.',
1947
+ inputSchema: {
1948
+ type: 'object',
1949
+ properties: {
1950
+ sources: { type: 'array', items: { type: 'string' }, description: 'Specific sources to collect (default: all)' },
1951
+ keywords: { type: 'array', items: { type: 'string' }, description: 'Keyword filter (OR match)' },
1952
+ limit: { type: 'number', description: 'Max items per source (default 20)' },
1953
+ baseDir: { type: 'string', description: 'Observer data directory' },
1954
+ },
1955
+ },
1956
+ },
1957
+ {
1958
+ name: 'mio.observer.ferment',
1959
+ description: 'Run fermentation engine: find recurring themes across recent observations and optionally write an essay.',
1960
+ inputSchema: {
1961
+ type: 'object',
1962
+ properties: {
1963
+ session: { type: 'string', description: 'Session label: morning/afternoon/night (default: afternoon)' },
1964
+ baseDir: { type: 'string', description: 'Observer data directory' },
1965
+ },
1966
+ },
1967
+ },
1968
+ {
1969
+ name: 'mio.observer.essays',
1970
+ description: 'List published or draft essays from the Observer.',
1971
+ inputSchema: {
1972
+ type: 'object',
1973
+ properties: {
1974
+ type: { type: 'string', description: 'published or draft (default: published)' },
1975
+ limit: { type: 'number', description: 'Max essays (default 10)' },
1976
+ baseDir: { type: 'string', description: 'Observer data directory' },
1977
+ },
1978
+ },
1979
+ },
1980
+ {
1981
+ name: 'mio.observer.dag',
1982
+ description: 'Get DAG state machine status: today\'s task, recent summaries, retryable tasks.',
1983
+ inputSchema: {
1984
+ type: 'object',
1985
+ properties: {
1986
+ days: { type: 'number', description: 'Days to look back (default 7)' },
1987
+ baseDir: { type: 'string', description: 'Observer data directory' },
1988
+ },
1989
+ },
1990
+ }] : []),
1991
+ ]
1992
+
1993
+ // ═══ Insight Self-Observation Handlers ═══
1994
+
1995
+ function insightStatus() {
1996
+ if (!insightStore) throw new Error('@akemi-mio/insight not installed')
1997
+ const all = insightStore.getAll()
1998
+ const unreported = insightStore.getUnreported()
1999
+ return {
2000
+ total: all.length,
2001
+ unreported: unreported.length,
2002
+ reported: all.length - unreported.length,
2003
+ highValue: insightStore.getHighValueUnreported(50, 0.7).length,
2004
+ }
2005
+ }
2006
+
2007
+ function insightList(args = {}) {
2008
+ if (!insightStore) throw new Error('@akemi-mio/insight not installed')
2009
+ let insights = insightStore.getAll()
2010
+ if (args.unreported) insights = insightStore.getUnreported()
2011
+ if (args.minScore) insights = insights.filter(i => i.score >= args.minScore)
2012
+ if (args.detector) insights = insights.filter(i => i.detector === args.detector)
2013
+ if (args.limit) insights = insights.slice(-args.limit)
2014
+ return insights
2015
+ }
2016
+
2017
+ function insightMarkReported(args = {}) {
2018
+ if (!insightStore) throw new Error('@akemi-mio/insight not installed')
2019
+ if (!args.ids || !Array.isArray(args.ids)) throw new Error('ids array required')
2020
+ for (const id of args.ids) insightStore.markReported(id)
2021
+ return { marked: args.ids.length }
2022
+ }
2023
+
2024
+ async function insightGenerate(args = {}) {
2025
+ if (!InsightGenerator) throw new Error('@akemi-mio/insight not installed')
2026
+ const generator = new InsightGenerator({ chatJson })
2027
+ const ctx = {
2028
+ memoryEntries: (args.memories || []).map(m => ({
2029
+ type: m.kind || m.type || 'note',
2030
+ content: m.content || '',
2031
+ createdAt: m.createdAt || m.timestamp || Date.now(),
2032
+ })),
2033
+ summaries: args.summaries || [],
2034
+ interactionCount: 0,
2035
+ plans: (args.plans || []).map(p => ({
2036
+ title: p.title || p.name || '',
2037
+ status: p.status || 'unknown',
2038
+ updatedAt: p.updatedAt || Date.now(),
2039
+ steps: p.steps || [],
2040
+ createdAt: p.createdAt || Date.now(),
2041
+ })),
2042
+ eventCount: 0,
2043
+ }
2044
+ const insights = await generator.generate(ctx)
2045
+ if (insightStore) {
2046
+ for (const i of insights) insightStore.addMany([i])
2047
+ }
2048
+ return { generated: insights.length, insights }
2049
+ }
2050
+
2051
+ // ═══ Observer Research Pipeline Handlers ═══
2052
+
2053
+ function getObserverStore(baseDir) {
2054
+ if (!ObserverStore) throw new Error('@akemi-mio/observer not installed')
2055
+ return new ObserverStore(baseDir || path.join(process.cwd(), '.local', 'observer'))
2056
+ }
2057
+
2058
+ function getObserverBaseDir(args) {
2059
+ return args.baseDir || path.join(process.cwd(), '.local', 'observer')
2060
+ }
2061
+
2062
+ function readJsonSafe(filePath) {
2063
+ if (!fs.existsSync(filePath)) return null
2064
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch { return null }
2065
+ }
2066
+
2067
+ function readJsonlSafe(filePath) {
2068
+ if (!fs.existsSync(filePath)) return []
2069
+ try {
2070
+ return fs.readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean)
2071
+ .map(l => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
2072
+ } catch { return [] }
2073
+ }
2074
+
2075
+ function observerWorldModel(args = {}) {
2076
+ const baseDir = getObserverBaseDir(args)
2077
+ const wmDir = path.join(baseDir, 'world_model')
2078
+ return {
2079
+ entities: readJsonSafe(path.join(wmDir, 'entities.json')) || [],
2080
+ events: readJsonSafe(path.join(wmDir, 'events.json')) || [],
2081
+ trends: readJsonSafe(path.join(wmDir, 'trends.json')) || [],
2082
+ narratives: readJsonSafe(path.join(wmDir, 'narratives.json')) || [],
2083
+ }
2084
+ }
2085
+
2086
+ function observerTrends(args = {}) {
2087
+ const baseDir = getObserverBaseDir(args)
2088
+ const trendsDir = path.join(baseDir, 'trends')
2089
+ if (!fs.existsSync(trendsDir)) return []
2090
+ if (args.date) {
2091
+ const report = readJsonSafe(path.join(trendsDir, `${args.date}.json`))
2092
+ return report ? [report] : []
2093
+ }
2094
+ const files = fs.readdirSync(trendsDir).filter(f => f.endsWith('.json')).sort().reverse()
2095
+ return files.slice(0, args.limit || 7).map(f => readJsonSafe(path.join(trendsDir, f))).filter(Boolean)
2096
+ }
2097
+
2098
+ function observerResearch(args = {}) {
2099
+ const baseDir = getObserverBaseDir(args)
2100
+ const researchDir = path.join(baseDir, 'research')
2101
+ if (!fs.existsSync(researchDir)) return []
2102
+ const files = fs.readdirSync(researchDir).filter(f => f.endsWith('.json')).sort().reverse()
2103
+ return files.slice(0, args.limit || 10).map(f => readJsonSafe(path.join(researchDir, f))).filter(Boolean)
2104
+ }
2105
+
2106
+ function observerInsights(args = {}) {
2107
+ const baseDir = getObserverBaseDir(args)
2108
+ const insightsDir = path.join(baseDir, 'insights')
2109
+ if (!fs.existsSync(insightsDir)) return []
2110
+ const files = fs.readdirSync(insightsDir).filter(f => f.endsWith('.json')).sort().reverse()
2111
+ return files.slice(0, args.limit || 20).map(f => readJsonSafe(path.join(insightsDir, f))).filter(Boolean)
2112
+ }
2113
+
2114
+ function observerStatus(args = {}) {
2115
+ const baseDir = getObserverBaseDir(args)
2116
+ const dirs = ['observations', 'trends', 'topics', 'research', 'insights', 'world_model', 'essays']
2117
+ const status = {}
2118
+ for (const d of dirs) {
2119
+ const dir = path.join(baseDir, d)
2120
+ if (fs.existsSync(dir)) {
2121
+ status[d] = fs.readdirSync(dir).filter(f => !f.startsWith('.')).length
2122
+ } else {
2123
+ status[d] = 0
2124
+ }
2125
+ }
2126
+ return status
2127
+ }
2128
+
2129
+ function observerCollect(args = {}) {
2130
+ if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
2131
+ const baseDir = getObserverBaseDir(args)
2132
+ const service = new ObserverService({ baseDir, collectorConfig: {} })
2133
+ const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
2134
+ const allObs = []
2135
+ for (const src of sources) {
2136
+ try {
2137
+ const collectFn = service.collectBySource ? service.collectBySource : service.collect
2138
+ if (collectFn) {
2139
+ const result = collectFn.call(service, [src], args.keywords || [], args.limit || 20)
2140
+ if (Array.isArray(result)) allObs.push(...result)
2141
+ else if (result && typeof result === 'object') {
2142
+ for (const [key, items] of Object.entries(result)) allObs.push(...items)
2143
+ }
2144
+ }
2145
+ } catch (e) {
2146
+ allObs.push({ source: src, error: e.message })
2147
+ }
2148
+ }
2149
+ return { collected: allObs.length, observations: allObs }
2150
+ }
2151
+
2152
+ function observerFerment(args = {}) {
2153
+ if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
2154
+ const baseDir = getObserverBaseDir(args)
2155
+ const service = new ObserverService({ baseDir, collectorConfig: {} })
2156
+ const session = args.session || 'afternoon'
2157
+ try {
2158
+ const fermentation = service.getFermentation()
2159
+ if (fermentation && fermentation.ferment) return fermentation.ferment(session)
2160
+ return { status: 'fermentation engine available but no ferment method' }
2161
+ } catch (e) {
2162
+ return { error: e.message }
2163
+ }
2164
+ }
2165
+
2166
+ function observerEssays(args = {}) {
2167
+ const baseDir = getObserverBaseDir(args)
2168
+ const type = args.type || 'published'
2169
+ const essaysDir = path.join(baseDir, 'essays', type)
2170
+ if (!fs.existsSync(essaysDir)) return []
2171
+ const files = fs.readdirSync(essaysDir).filter(f => f.endsWith('.md')).sort().reverse()
2172
+ return files.slice(0, args.limit || 10).map(f => {
2173
+ const content = fs.readFileSync(path.join(essaysDir, f), 'utf8')
2174
+ return { file: f, type, content, created: fs.statSync(path.join(essaysDir, f)).birthtime.toISOString() }
2175
+ })
2176
+ }
2177
+
2178
+ function observerDag(args = {}) {
2179
+ const baseDir = getObserverBaseDir(args)
2180
+ const days = args.days || 7
2181
+ const summaries = []
2182
+ const todayDate = new Date()
2183
+ for (let i = 0; i < days; i++) {
2184
+ const d = new Date(todayDate)
2185
+ d.setDate(d.getDate() - i)
2186
+ const dateStr = d.toISOString().slice(0, 10)
2187
+ const filePath = path.join(baseDir, 'summaries', `${dateStr}.jsonl`)
2188
+ const items = readJsonlSafe(filePath)
2189
+ if (items.length) summaries.push({ date: dateStr, summaries: items })
2190
+ }
2191
+ return { summaries, summaryCount: summaries.reduce((a, s) => a + s.summaries.length, 0) }
2192
+ }
2193
+
1800
2194
  async function callTool(name, args = {}) {
1801
2195
  switch (name) {
1802
2196
  case 'mio.memory.query':
@@ -1833,77 +2227,65 @@ async function callTool(name, args = {}) {
1833
2227
  return listAgents(args)
1834
2228
  case 'mio.agent.report':
1835
2229
  return reportAgent(args)
1836
- case 'mio.evolution.report':
1837
- return evolutionReport(args)
1838
- case 'mio.task.record_outcome':
1839
- return recordTaskOutcome(args)
1840
- case 'mio.idea.generate':
1841
- return handleIdeaGenerate(args)
1842
- case 'mio.idea.list':
1843
- return listIdeas(args)
1844
- case 'mio.evolution.suggest':
1845
- return evolutionSuggest(args)
1846
- case 'mio.dashboard.start':
1847
- return handleDashboardStart(args)
1848
- case 'mio.dashboard.stop':
1849
- return handleDashboardStop()
1850
- case 'mio.dashboard.status':
1851
- return handleDashboardStatus()
1852
- default:
2230
+ case 'mio.evolution.report':
2231
+ return evolutionReport(args)
2232
+ case 'mio.evolution.status':
2233
+ return getEvolutionStatus()
2234
+ case 'mio.host.capabilities':
2235
+ return listHostCapabilities()
2236
+ case 'mio.evolution.shadow.record':
2237
+ return evolutionCutover.recordShadowComparison(args)
2238
+ case 'mio.evolution.dual_write.record':
2239
+ return evolutionCutover.recordDualWrite(args)
2240
+ case 'mio.evolution.cutover.readiness':
2241
+ return evolutionCutover.cutoverReadiness(args)
2242
+ case 'mio.evolution.migration.plan':
2243
+ return evolutionCutover.migrationPlan(args)
2244
+ case 'mio.evolution.authority.plan':
2245
+ return evolutionCutover.authorityPlan(args)
2246
+ case 'mio.evolution.cutover.apply':
2247
+ return evolutionCutover.applyCutover(args)
2248
+ case 'mio.task.record_outcome':
2249
+ return recordTaskOutcome(args)
2250
+ case 'mio.creativity.status':
2251
+ return creativityEngine.status()
2252
+ case 'mio.creativity.list':
2253
+ return creativityEngine.list(args)
2254
+ case 'mio.creativity.generate':
2255
+ return creativityEngine.generate(args.sources, args.strategy)
2256
+ case 'mio.creativity.ferment':
2257
+ return creativityEngine.ferment(args.limit)
2258
+ case 'mio.insight.status':
2259
+ return insightStatus()
2260
+ case 'mio.insight.list':
2261
+ return insightList(args)
2262
+ case 'mio.insight.mark_reported':
2263
+ return insightMarkReported(args)
2264
+ case 'mio.insight.generate':
2265
+ return insightGenerate(args)
2266
+ case 'mio.observer.world_model':
2267
+ return observerWorldModel(args)
2268
+ case 'mio.observer.trends':
2269
+ return observerTrends(args)
2270
+ case 'mio.observer.research':
2271
+ return observerResearch(args)
2272
+ case 'mio.observer.insights':
2273
+ return observerInsights(args)
2274
+ case 'mio.observer.status':
2275
+ return observerStatus(args)
2276
+ case 'mio.observer.collect':
2277
+ return observerCollect(args)
2278
+ case 'mio.observer.ferment':
2279
+ return observerFerment(args)
2280
+ case 'mio.observer.essays':
2281
+ return observerEssays(args)
2282
+ case 'mio.observer.dag':
2283
+ return observerDag(args)
2284
+ default:
1853
2285
  throw new Error(`Unknown tool: ${name}`)
1854
2286
  }
1855
2287
  }
1856
2288
 
1857
- const DASHBOARD_PORT = 8787
1858
- const dashboardPidPath = () => path.join(dataDir, 'dashboard.pid')
1859
-
1860
- function isDashboardRunning() {
1861
- const pidFile = dashboardPidPath()
1862
- if (!fs.existsSync(pidFile)) return false
1863
- try {
1864
- const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
1865
- process.kill(pid, 0)
1866
- return true
1867
- } catch (_) {
1868
- return false
1869
- }
1870
- }
1871
-
1872
- function handleDashboardStart(args = {}) {
1873
- const port = parseInt(args.port || process.env.MIO_DASHBOARD_PORT || DASHBOARD_PORT, 10)
1874
- const url = 'http://localhost:' + port
1875
- if (isDashboardRunning()) {
1876
- if (args.open) { try { require('child_process').exec('start ' + url) } catch (_) {} }
1877
- return { url: url, running: true, message: 'Dashboard already running at ' + url }
1878
- }
1879
- const serverScript = path.join(__dirname, 'dashboard-server.js')
1880
- const child = require('child_process').spawn(process.execPath, [serverScript, String(port)], {
1881
- env: Object.assign({}, process.env, { MIO_DATA_DIR: dataDir }),
1882
- detached: true,
1883
- stdio: 'ignore',
1884
- })
1885
- child.unref()
1886
- fs.writeFileSync(dashboardPidPath(), String(child.pid))
1887
- if (args.open) { try { require('child_process').exec('start ' + url) } catch (_) {} }
1888
- return { url: url, running: true, pid: child.pid, message: 'Dashboard started at ' + url }
1889
- }
1890
-
1891
- function handleDashboardStop() {
1892
- const pidFile = dashboardPidPath()
1893
- if (!fs.existsSync(pidFile)) return { running: false, message: 'Dashboard not running.' }
1894
- try {
1895
- const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
1896
- process.kill(pid, 'SIGTERM')
1897
- } catch (_) {}
1898
- try { fs.unlinkSync(pidFile) } catch (_) {}
1899
- return { running: false, message: 'Dashboard stopped.' }
1900
- }
1901
-
1902
- function handleDashboardStatus() {
1903
- const running = isDashboardRunning()
1904
- return { running: running, url: running ? 'http://localhost:' + DASHBOARD_PORT : null }
1905
- }
1906
-
1907
2289
  function jsonRpcResult(id, result) {
1908
2290
  return { jsonrpc: '2.0', id, result }
1909
2291
  }
@@ -2218,65 +2600,4 @@ function recordTaskOutcome(args = {}) {
2218
2600
  }
2219
2601
  }
2220
2602
 
2221
-
2222
- function evolutionSuggest(args = {}) {
2223
- const project = args.project || projectName()
2224
- const now = Date.now()
2225
- const memories = readJsonl(memoryPath)
2226
- const traces = readJsonl(tracePath)
2227
- const reuses = readJsonl(experienceReusePath)
2228
- const ideas = fs.existsSync(ideasPath)
2229
- ? fs.readFileSync(ideasPath, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l) } catch (_) { return null } }).filter(Boolean)
2230
- : []
2231
- const agentIds = new Set()
2232
- traces.forEach((t) => { if (t.agent) agentIds.add(t.agent) })
2233
- const outcomes = traces.filter((t) => t.event_type === 'task_outcome')
2234
- const successes = outcomes.filter((t) => t.outcome === 'success').length
2235
- const successRate = outcomes.length > 0 ? Math.round((successes / outcomes.length) * 100) : 0
2236
- const verifiedReuses = reuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
2237
- const pendingReuses = reuses.filter((r) => r.source === 'auto_claim' && !r.confirmed)
2238
- const decisions = memories.filter((m) => m.kind === 'decision')
2239
- const problems = memories.filter((m) => m.kind === 'problem')
2240
- const shortMemories = memories.filter((m) => String(m.content || '').trim().length < 20)
2241
- const ideasWithoutMemory = ideas.filter((i) => !i.provenance?.relatedMemoryIds?.length)
2242
- const suggestions = []
2243
-
2244
- if (agentIds.size >= 2) {
2245
- const agentStats = []
2246
- for (const id of agentIds) {
2247
- const ao = outcomes.filter((t) => t.agent === id)
2248
- const as = ao.filter((t) => t.outcome === 'success').length
2249
- agentStats.push({ id, total: ao.length, success: as, rate: ao.length > 0 ? Math.round(as / ao.length * 100) : 0 })
2250
- }
2251
- agentStats.sort((a, b) => b.rate - a.rate)
2252
- const best = agentStats[0], worst = agentStats[agentStats.length - 1]
2253
- if (best.total >= 5 && worst.total >= 5 && best.rate - worst.rate >= 15) {
2254
- suggestions.push({ category: 'routing', priority: 'high', title: 'Route tasks to ' + best.id, description: best.id + ' has ' + best.rate + '% success rate vs ' + worst.id + ' at ' + worst.rate + '%.', nextAction: 'Update task routing rules to prefer ' + best.id, impact: 'Could improve overall success rate by ~' + Math.round((best.rate - worst.rate) * 0.3) + '%', basedOn: 'task_outcome comparison' })
2255
- }
2256
- }
2257
- if (shortMemories.length > 5 && memories.length > 10) {
2258
- suggestions.push({ category: 'memory', priority: 'medium', title: 'Enrich ' + shortMemories.length + ' short memory records', description: Math.round(shortMemories.length / memories.length * 100) + '% of memories have content < 20 chars.', nextAction: 'Review and expand short memory records, or archive irrelevant ones.', impact: 'Improves memory.query recall precision', basedOn: 'memory.jsonl analysis' })
2259
- }
2260
- if (pendingReuses.length >= 5) {
2261
- suggestions.push({ category: 'evidence', priority: 'medium', title: 'Confirm ' + pendingReuses.length + ' pending auto-claims', description: pendingReuses.length + ' auto-claimed reuses await confirmation.', nextAction: 'Call mio.experience.confirm on each auto_claim id.', impact: 'Increases verified reuse count toward Phase 0 threshold', basedOn: 'experience_reuse.jsonl' })
2262
- }
2263
- const crossAgent = verifiedReuses.filter((r) => r.sourceAgent !== r.targetAgent)
2264
- if (agentIds.size >= 2 && crossAgent.length === 0) {
2265
- suggestions.push({ category: 'interop', priority: 'high', title: 'Enable cross-agent experience sharing', description: 'Multiple agents exist but no cross-agent reuse detected.', nextAction: 'Ensure all agents query shared memory before designing solutions.', impact: 'Unlocks core Agent Control Plane value', basedOn: 'experience_reuse.jsonl' })
2266
- }
2267
- if (ideas.length > 0 && ideasWithoutMemory.length > ideas.length * 0.5) {
2268
- suggestions.push({ category: 'creativity', priority: 'low', title: 'Improve idea grounding (' + ideasWithoutMemory.length + ' ungrounded)', description: Math.round(ideasWithoutMemory.length / ideas.length * 100) + '% of ideas lack related memory context.', nextAction: 'Record more decision/problem memories to enrich the idea pipeline.', impact: 'Higher-quality, more context-aware ideas', basedOn: 'ideas.jsonl provenance' })
2269
- }
2270
- if (problems.length >= 3) {
2271
- const recent = problems.filter((p) => (now - new Date(p.timestamp).getTime()) < 7 * 86400000)
2272
- if (recent.length >= 2) {
2273
- suggestions.push({ category: 'health', priority: 'high', title: recent.length + ' problems in last 7 days', description: 'Recurring problems may indicate systemic issues.', nextAction: 'Review recent problems, identify root causes, create decision records.', impact: 'Prevents problem accumulation', basedOn: 'memory.jsonl recent problems' })
2274
- }
2275
- }
2276
- suggestions.sort((a, b) => ({ high: 0, medium: 1, low: 2 }[a.priority] || 2) - ({ high: 0, medium: 1, low: 2 }[b.priority] || 2))
2277
- const record = { id: 'evo_' + Date.now(), timestamp: new Date().toISOString(), project, metrics: { agents: agentIds.size, tasks: outcomes.length, successRate, memories: memories.length, decisions: decisions.length, problems: problems.length, verifiedReuses: verifiedReuses.length, pendingReuses: pendingReuses.length, ideas: ideas.length }, suggestions }
2278
- try { fs.appendFileSync(suggestionsPath, JSON.stringify(record) + '\n') } catch (_) {}
2279
- return record
2280
- }
2281
-
2282
2603
  module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }