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.
@@ -0,0 +1,397 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const crypto = require('crypto')
6
+
7
+ // ═══════════════════════════════════════════════
8
+ // JSONL Store — ideas, combos, experiments
9
+ // ═══════════════════════════════════════════════
10
+
11
+ class CreativityStore {
12
+ constructor(dataDir) {
13
+ this.dataDir = dataDir
14
+ this.hypothesesPath = path.join(dataDir, 'creativity-hypotheses.jsonl')
15
+ this.combosPath = path.join(dataDir, 'creativity-combos.jsonl')
16
+ this.experimentsPath = path.join(dataDir, 'creativity-experiments.jsonl')
17
+ this._hypotheses = null
18
+ this._combos = null
19
+ this._experiments = null
20
+ }
21
+
22
+ _ensureDir() {
23
+ if (!fs.existsSync(this.dataDir)) {
24
+ fs.mkdirSync(this.dataDir, { recursive: true })
25
+ }
26
+ }
27
+
28
+ _readJsonl(filePath) {
29
+ if (!fs.existsSync(filePath)) return []
30
+ return fs.readFileSync(filePath, 'utf-8')
31
+ .split('\n')
32
+ .filter(Boolean)
33
+ .map(line => {
34
+ try { return JSON.parse(line) } catch { return null }
35
+ })
36
+ .filter(Boolean)
37
+ }
38
+
39
+ _appendJsonl(filePath, record) {
40
+ this._ensureDir()
41
+ fs.appendFileSync(filePath, JSON.stringify(record) + '\n')
42
+ }
43
+
44
+ getHypotheses(opts = {}) {
45
+ if (this._hypotheses === null) {
46
+ this._hypotheses = this._readJsonl(this.hypothesesPath)
47
+ }
48
+ let list = this._hypotheses
49
+ if (opts.status) list = list.filter(h => h.status === opts.status)
50
+ if (opts.limit) list = list.slice(-opts.limit)
51
+ return list
52
+ }
53
+
54
+ addHypothesis(h) {
55
+ this._appendJsonl(this.hypothesesPath, h)
56
+ if (this._hypotheses) this._hypotheses.push(h)
57
+ }
58
+
59
+ updateHypothesis(id, patch) {
60
+ const all = this.getHypotheses()
61
+ const idx = all.findIndex(h => h.id === id)
62
+ if (idx === -1) return false
63
+ Object.assign(all[idx], patch)
64
+ this._hypotheses = all
65
+ this._rewriteFile(this.hypothesesPath, all)
66
+ return true
67
+ }
68
+
69
+ getCombos(limit) {
70
+ if (this._combos === null) {
71
+ this._combos = this._readJsonl(this.combosPath)
72
+ }
73
+ return limit ? this._combos.slice(-limit) : this._combos
74
+ }
75
+
76
+ addCombo(c) {
77
+ this._appendJsonl(this.combosPath, c)
78
+ if (this._combos) this._combos.push(c)
79
+ }
80
+
81
+ getExperiments() {
82
+ if (this._experiments === null) {
83
+ this._experiments = this._readJsonl(this.experimentsPath)
84
+ }
85
+ return this._experiments
86
+ }
87
+
88
+ addExperiment(e) {
89
+ this._appendJsonl(this.experimentsPath, e)
90
+ if (this._experiments) this._experiments.push(e)
91
+ }
92
+
93
+ count() {
94
+ return {
95
+ hypotheses: this.getHypotheses().length,
96
+ combos: this.getCombos().length,
97
+ experiments: this.getExperiments().length,
98
+ }
99
+ }
100
+
101
+ _rewriteFile(filePath, records) {
102
+ this._ensureDir()
103
+ fs.writeFileSync(filePath, records.map(r => JSON.stringify(r)).join('\n') + '\n')
104
+ }
105
+ }
106
+
107
+ // ═══════════════════════════════════════════════
108
+ // Concept Mixer — pair sources for novel combos
109
+ // ═══════════════════════════════════════════════
110
+
111
+ function mulberry32(seed) {
112
+ let t = seed | 0
113
+ return () => {
114
+ t = (t + 0x6D2B79F5) | 0
115
+ let x = Math.imul(t ^ (t >>> 15), 1 | t)
116
+ x ^= x + Math.imul(x ^ (x >>> 7), 61 | x)
117
+ return ((x ^ (x >>> 14)) >>> 0) / 4294967296
118
+ }
119
+ }
120
+
121
+ function mixConcepts(sources, exploredPairs, rng) {
122
+ if (sources.length < 2) return []
123
+ const pairs = []
124
+ for (let i = 0; i < sources.length; i++) {
125
+ for (let j = i + 1; j < sources.length; j++) {
126
+ const key = `${sources[i].name}||${sources[j].name}`
127
+ const reverseKey = `${sources[j].name}||${sources[i].name}`
128
+ if (exploredPairs.has(key) || exploredPairs.has(reverseKey)) continue
129
+ pairs.push([sources[i], sources[j]])
130
+ }
131
+ }
132
+ // Shuffle and take top pairs
133
+ for (let i = pairs.length - 1; i > 0; i--) {
134
+ const j = Math.floor(rng() * (i + 1))
135
+ ;[pairs[i], pairs[j]] = [pairs[j], pairs[i]]
136
+ }
137
+ return pairs.slice(0, Math.min(5, pairs.length))
138
+ }
139
+
140
+ // ═══════════════════════════════════════════════
141
+ // Novelty Scorer — reject duplicates
142
+ // ═══════════════════════════════════════════════
143
+
144
+ function bigramSet(text) {
145
+ const s = new Set()
146
+ const lower = text.toLowerCase()
147
+ for (let i = 0; i < lower.length - 1; i++) {
148
+ s.add(lower.slice(i, i + 2))
149
+ }
150
+ return s
151
+ }
152
+
153
+ function jaccard(a, b) {
154
+ if (a.size === 0 && b.size === 0) return 1
155
+ let inter = 0
156
+ for (const x of a) if (b.has(x)) inter++
157
+ return inter / (a.size + b.size - inter)
158
+ }
159
+
160
+ function evaluateNovelty(idea, recent, rejected) {
161
+ const ideaBigrams = bigramSet(idea.title + ' ' + idea.idea)
162
+ for (const r of recent) {
163
+ const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
164
+ if (sim > 0.7) {
165
+ return { shouldReject: true, rejectReason: `too similar to recent: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
166
+ }
167
+ }
168
+ for (const r of rejected) {
169
+ const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
170
+ if (sim > 0.6) {
171
+ return { shouldReject: true, rejectReason: `matches rejected: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
172
+ }
173
+ }
174
+ return { shouldReject: false, adjustedNovelty: Math.min(100, idea.novelty + 5) }
175
+ }
176
+
177
+ // ═══════════════════════════════════════════════
178
+ // Creativity Engine — main orchestrator
179
+ // ═══════════════════════════════════════════════
180
+
181
+ const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
182
+
183
+ class CreativityEngine {
184
+ constructor(dataDir, chatJson) {
185
+ this.store = new CreativityStore(dataDir)
186
+ this.chatJson = chatJson
187
+ this.strategyIndex = 0
188
+ this.exploredPairs = new Set()
189
+ this.rng = mulberry32(Date.now())
190
+ }
191
+
192
+ async generate(sourceLabels, strategy) {
193
+ const sources = sourceLabels.map(s => ({
194
+ name: s.name || 'unnamed',
195
+ content: s.content || '',
196
+ type: s.type || 'knowledge',
197
+ weight: s.weight || 0.5,
198
+ }))
199
+
200
+ if (sources.length < 2) {
201
+ return { ideas: [], reason: 'need at least 2 sources' }
202
+ }
203
+
204
+ const strat = strategy || STRATEGY_CYCLE[this.strategyIndex % STRATEGY_CYCLE.length]
205
+ this.strategyIndex++
206
+
207
+ // Mix concepts
208
+ const recent = this.store.getHypotheses({ limit: 30 })
209
+ const rejected = this.store.getHypotheses({ status: 'rejected', limit: 50 })
210
+ const exploredPairs = new Set(recent.flatMap(h => (h.sourceLabels || []).map((l, i, a) => i > 0 ? `${a[i-1]}||${l}` : '')).filter(Boolean))
211
+ const pairs = mixConcepts(sources, exploredPairs, this.rng.bind(this))
212
+
213
+ if (pairs.length === 0) {
214
+ return { ideas: [], reason: 'all pairs already explored' }
215
+ }
216
+
217
+ // Generate hypotheses via LLM
218
+ const ideas = []
219
+ for (const [a, b] of pairs) {
220
+ const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5))
221
+ try {
222
+ const result = await this.chatJson(prompt, {
223
+ system: `You are a creative hypothesis generator. Output JSON with fields: title, idea, expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100). Generate ONE novel hypothesis by combining the given concepts.`,
224
+ temperature: strat === 'explore' ? 0.8 : 0.4,
225
+ })
226
+
227
+ if (result.data) {
228
+ const h = {
229
+ id: crypto.randomUUID(),
230
+ title: result.data.title || 'Untitled',
231
+ idea: result.data.idea || '',
232
+ expectedBenefit: result.data.expectedBenefit || '',
233
+ risk: result.data.risk || '',
234
+ novelty: Math.min(100, Math.max(0, result.data.novelty || 50)),
235
+ feasibility: Math.min(100, Math.max(0, result.data.feasibility || 50)),
236
+ impact: Math.min(100, Math.max(0, result.data.impact || 50)),
237
+ sourceLabels: [a.name, b.name],
238
+ status: 'active',
239
+ createdAt: Date.now(),
240
+ strategy: strat,
241
+ }
242
+
243
+ // Novelty check
244
+ const verdict = evaluateNovelty(h, recent, rejected)
245
+ if (verdict.shouldReject) {
246
+ h.status = 'rejected'
247
+ h.rejectionReason = verdict.rejectReason
248
+ } else {
249
+ h.novelty = verdict.adjustedNovelty
250
+ }
251
+
252
+ this.store.addHypothesis(h)
253
+ ideas.push(h)
254
+
255
+ // Record combo
256
+ this.store.addCombo({
257
+ id: crypto.randomUUID(),
258
+ sources: [a.name, b.name],
259
+ description: h.title,
260
+ createdAt: Date.now(),
261
+ })
262
+ }
263
+ } catch (err) {
264
+ // LLM call failed, skip this pair
265
+ }
266
+ }
267
+
268
+ return { ideas, strategy: strat, pairsAttempted: pairs.length }
269
+ }
270
+
271
+ async ferment(limit = 5) {
272
+ const fermentable = this.store.getHypotheses({ status: 'active' })
273
+ .concat(this.store.getHypotheses({ status: 'draft' }))
274
+ .slice(-limit)
275
+
276
+ if (fermentable.length === 0) {
277
+ return { fermented: 0, reason: 'no fermentable hypotheses' }
278
+ }
279
+
280
+ const results = []
281
+ for (const h of fermentable) {
282
+ const prompt = `Review and refine this hypothesis. Current state:
283
+ Title: ${h.title}
284
+ Idea: ${h.idea}
285
+ Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}
286
+
287
+ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impact, verdict (promote/keep/reject/merge), reason.`
288
+
289
+ try {
290
+ const result = await this.chatJson(prompt, {
291
+ system: 'You are a creative hypothesis reviewer. Output JSON with refined scores and a verdict.',
292
+ temperature: 0.3,
293
+ })
294
+
295
+ if (result.data) {
296
+ const patch = {
297
+ title: result.data.title || h.title,
298
+ idea: result.data.idea || h.idea,
299
+ expectedBenefit: result.data.expectedBenefit || h.expectedBenefit,
300
+ risk: result.data.risk || h.risk,
301
+ novelty: Math.min(100, Math.max(0, result.data.novelty || h.novelty)),
302
+ feasibility: Math.min(100, Math.max(0, result.data.feasibility || h.feasibility)),
303
+ impact: Math.min(100, Math.max(0, result.data.impact || h.impact)),
304
+ fermentedAt: Date.now(),
305
+ fermentCount: (h.fermentCount || 0) + 1,
306
+ }
307
+
308
+ if (result.data.verdict === 'reject') {
309
+ patch.status = 'rejected'
310
+ } else if (result.data.verdict === 'promote' && (h.novelty + h.feasibility + h.impact) > 200) {
311
+ patch.status = 'validated'
312
+ }
313
+
314
+ this.store.updateHypothesis(h.id, patch)
315
+ results.push({ id: h.id, title: h.title, verdict: result.data.verdict, reason: result.data.reason })
316
+ }
317
+ } catch (err) {
318
+ // skip
319
+ }
320
+ }
321
+
322
+ return { fermented: results.length, results }
323
+ }
324
+
325
+ status() {
326
+ const count = this.store.count()
327
+ const active = this.store.getHypotheses({ status: 'active' })
328
+ const validated = this.store.getHypotheses({ status: 'validated' })
329
+ const rejected = this.store.getHypotheses({ status: 'rejected' })
330
+
331
+ return {
332
+ ...count,
333
+ active: active.length,
334
+ validated: validated.length,
335
+ rejected: rejected.length,
336
+ recentIdeas: active.slice(-5).map(h => ({
337
+ id: h.id,
338
+ title: h.title,
339
+ novelty: h.novelty,
340
+ feasibility: h.feasibility,
341
+ impact: h.impact,
342
+ score: h.novelty + h.feasibility + h.impact,
343
+ })),
344
+ }
345
+ }
346
+
347
+ list(opts = {}) {
348
+ return this.store.getHypotheses(opts).map(h => ({
349
+ id: h.id,
350
+ title: h.title,
351
+ idea: h.idea,
352
+ status: h.status,
353
+ novelty: h.novelty,
354
+ feasibility: h.feasibility,
355
+ impact: h.impact,
356
+ score: h.novelty + h.feasibility + h.impact,
357
+ sourceLabels: h.sourceLabels,
358
+ createdAt: h.createdAt,
359
+ fermentedAt: h.fermentedAt,
360
+ fermentCount: h.fermentCount,
361
+ }))
362
+ }
363
+
364
+ _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
365
+ let prompt = `Combine these two concepts into ONE novel hypothesis:\n\n`
366
+ prompt += `Concept A: "${a.name}" — ${a.content}\n`
367
+ prompt += `Concept B: "${b.name}" — ${b.content}\n\n`
368
+ prompt += `Strategy: ${strategy}\n`
369
+
370
+ if (strategy === 'explore') {
371
+ prompt += `Cross-domain combination. Emphasize structural differences and unexpected connections.\n`
372
+ } else if (strategy === 'signal') {
373
+ prompt += `Inject external signals. Be provocative and challenge assumptions.\n`
374
+ } else {
375
+ prompt += `Stable combination. Conservative refinement of existing ideas.\n`
376
+ }
377
+
378
+ if (recentHypotheses.length > 0) {
379
+ prompt += `\nRecent ideas (avoid duplicates):\n`
380
+ for (const h of recentHypotheses) {
381
+ prompt += `- "${h.title}" (novelty=${h.novelty})\n`
382
+ }
383
+ }
384
+
385
+ if (rejectedHypotheses.length > 0) {
386
+ prompt += `\nRejected ideas (learn from failures):\n`
387
+ for (const h of rejectedHypotheses) {
388
+ prompt += `- "${h.title}" — ${h.risk}\n`
389
+ }
390
+ }
391
+
392
+ prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100}`
393
+ return prompt
394
+ }
395
+ }
396
+
397
+ module.exports = { CreativityEngine, CreativityStore }
@@ -0,0 +1,119 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+
5
+ function requireWorkspacePackage(name, fallbackPath) {
6
+ try {
7
+ return require(name)
8
+ } catch (error) {
9
+ if (!error || error.code !== 'MODULE_NOT_FOUND') throw error
10
+ return require(fallbackPath)
11
+ }
12
+ }
13
+
14
+ const {
15
+ assessCutoverReadiness,
16
+ createAuthoritySwitchDryRun,
17
+ createAuthoritySwitchPlan,
18
+ createDualWriteRunner,
19
+ createStateMigrationPlan,
20
+ runShadowComparison,
21
+ } = requireWorkspacePackage(
22
+ '@akemi-mio/evolution-scheduler',
23
+ path.resolve(__dirname, '..', '..', 'evolution-scheduler'),
24
+ )
25
+
26
+ function createEvolutionCutoverTools({ dataDir, appendJsonl, readJsonl, projectName }) {
27
+ const shadowPath = path.join(dataDir, 'evolution_shadow.jsonl')
28
+ const dualWritePath = path.join(dataDir, 'evolution_dual_write.jsonl')
29
+
30
+ function projectFromArgs(args) {
31
+ return args.project || projectName()
32
+ }
33
+
34
+ async function recordShadowComparison(args = {}) {
35
+ const project = projectFromArgs(args)
36
+ const comparison = await runShadowComparison({
37
+ input: args.input || {},
38
+ legacy: { run: () => args.legacy },
39
+ modular: { run: () => args.modular },
40
+ })
41
+ const record = {
42
+ id: `shadow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
43
+ timestamp: new Date().toISOString(),
44
+ project,
45
+ label: String(args.label || 'shadow comparison'),
46
+ ...comparison,
47
+ }
48
+ appendJsonl(shadowPath, record)
49
+ return record
50
+ }
51
+
52
+ async function recordDualWrite(args = {}) {
53
+ const project = projectFromArgs(args)
54
+ const runner = createDualWriteRunner({
55
+ authoritative: args.authoritative,
56
+ legacyWriter: { write: () => args.legacyResult },
57
+ modularWriter: { write: () => args.modularResult },
58
+ })
59
+ const result = await runner.write(args.record || {})
60
+ const stored = {
61
+ id: `dual_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
62
+ timestamp: new Date().toISOString(),
63
+ project,
64
+ label: String(args.label || 'dual write'),
65
+ ...result,
66
+ }
67
+ appendJsonl(dualWritePath, stored)
68
+ return stored
69
+ }
70
+
71
+ function cutoverReadiness(args = {}) {
72
+ const project = projectFromArgs(args)
73
+ const shadowRuns = readJsonl(shadowPath).filter((record) => record.project === project)
74
+ const dualWriteRuns = readJsonl(dualWritePath).filter((record) => record.project === project)
75
+ return {
76
+ project,
77
+ ...assessCutoverReadiness({
78
+ shadowRuns,
79
+ dualWriteRuns,
80
+ minShadowRuns: Number.isFinite(args.minShadowRuns) ? args.minShadowRuns : undefined,
81
+ maxMismatchRate: Number.isFinite(args.maxMismatchRate) ? args.maxMismatchRate : undefined,
82
+ }),
83
+ }
84
+ }
85
+
86
+ function migrationPlan(args = {}) {
87
+ return createStateMigrationPlan({
88
+ legacyRecords: args.legacyRecords,
89
+ modularRecords: args.modularRecords,
90
+ })
91
+ }
92
+
93
+ function authorityPlan(args = {}) {
94
+ return createAuthoritySwitchPlan({
95
+ readiness: args.readiness,
96
+ from: args.from,
97
+ to: args.to,
98
+ })
99
+ }
100
+
101
+ function applyCutover(args = {}) {
102
+ return createAuthoritySwitchDryRun({
103
+ dryRun: args.dryRun,
104
+ project: projectFromArgs(args),
105
+ plan: args.plan || args.authorityPlan,
106
+ })
107
+ }
108
+
109
+ return {
110
+ recordShadowComparison,
111
+ recordDualWrite,
112
+ cutoverReadiness,
113
+ migrationPlan,
114
+ authorityPlan,
115
+ applyCutover,
116
+ }
117
+ }
118
+
119
+ module.exports = { createEvolutionCutoverTools }
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const adapters = {
4
+ codex: require('../adapters/codex.js'),
5
+ opencode: require('../adapters/opencode.js'),
6
+ workbuddy: require('../adapters/workbuddy.js'),
7
+ hermes: require('../adapters/hermes.js'),
8
+ }
9
+
10
+ const HOST_CAPABILITIES = [
11
+ {
12
+ name: 'codex',
13
+ capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'policy-check', 'experience-reuse', 'runtime-status'],
14
+ },
15
+ {
16
+ name: 'opencode',
17
+ capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'policy-check', 'experience-reuse', 'runtime-status'],
18
+ },
19
+ {
20
+ name: 'workbuddy',
21
+ capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
22
+ },
23
+ {
24
+ name: 'hermes',
25
+ capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
26
+ },
27
+ ]
28
+
29
+ function safeInstalled(adapter) {
30
+ try {
31
+ return Boolean(adapter && typeof adapter.isInstalled === 'function' && adapter.isInstalled())
32
+ } catch (_) {
33
+ return false
34
+ }
35
+ }
36
+
37
+ function listHostCapabilities(adapterOverrides = adapters) {
38
+ return {
39
+ schemaVersion: 1,
40
+ generatedAt: new Date().toISOString(),
41
+ hosts: HOST_CAPABILITIES.map((host) => ({
42
+ name: host.name,
43
+ installed: safeInstalled(adapterOverrides[host.name]),
44
+ capabilities: host.capabilities,
45
+ mutationMode: 'host-capability',
46
+ })),
47
+ }
48
+ }
49
+
50
+ module.exports = { HOST_CAPABILITIES, listHostCapabilities }