mio-agent-runtime 0.14.1 → 0.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -34,9 +34,9 @@
34
34
  "@akemi-mio/evolution-strategy": "^0.1.0",
35
35
  "@akemi-mio/evolution-safety": "^0.1.0",
36
36
  "@akemi-mio/evolution-scheduler": "^0.1.0",
37
- "@akemi-mio/observer": "^0.1.4",
37
+ "@akemi-mio/observer": "^0.1.5",
38
38
  "@akemi-mio/insight": "^0.1.1",
39
- "@akemi-mio/creativity": "^0.1.0"
39
+ "@akemi-mio/creativity": "^0.1.1"
40
40
  },
41
41
  "main": "index.js",
42
42
  "directories": {
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const crypto = require('crypto')
6
+ const { readJsonl } = require('./memory-store.js')
6
7
  const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
7
8
 
8
9
  // Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
@@ -257,8 +258,55 @@ function listRow(h) {
257
258
  }
258
259
  }
259
260
 
261
+ // --- read-side adoption join (idea b19bada4, design discussion 1d2e22e4 D3) ---
262
+ // The store never gains an `adopted` status (D1/D4, see adoptHypothesis at the
263
+ // bottom of this file): adoption is joined at READ time from the two evidence
264
+ // axes the write side owns -- an append-only `creativity.adopt` trace event
265
+ // (payload.hypothesisId = the CLAIM, written by mio.creativity.adopt) and a
266
+ // `hypothesis:<id>` tag on a memory record (the EVIDENCE, injected by
267
+ // mio.memory.record's hypothesisId parameter). The engine sits in
268
+ // <dataRoot>/creativity on both hosts (MCP: path.join(dataDir, 'creativity');
269
+ // CLI: path.join(MIO_HOME, 'creativity')), so the two evidence files are one
270
+ // dirname up -- the same dataRoot convention adoptHypothesis documents.
271
+ // Missing files are fine: readJsonl returns [] for them.
272
+ //
273
+ // The counts are informational joins, NOT ADR-017 metrics: no rate and no
274
+ // threshold by design (D3 -- adoption rate was explicitly out of scope).
275
+ // `sources` follows the routeAdoption pattern (evaluation-store.js): names
276
+ // each axis' backing file and that it is durable, so a reader can tell
277
+ // "0 adopted" apart from "evidence not kept". claimed/evidenced are raw
278
+ // distinct ids (they can include ids no longer in the store); adopted is the
279
+ // inner join against stored hypotheses, the only count that answers "which
280
+ // surviving hypotheses were adopted".
281
+ const ADOPTION_TAG_PREFIX = 'hypothesis:'
282
+
283
+ const ADOPTION_SOURCES = Object.freeze({
284
+ events: { file: 'traces.jsonl', durable: true },
285
+ memory: { file: 'memory.jsonl', durable: true },
286
+ })
287
+
288
+ function readAdoptionEvidence(rootDir) {
289
+ const claimed = new Set()
290
+ for (const record of readJsonl(path.join(rootDir, 'traces.jsonl'))) {
291
+ if (!record || record.event_type !== 'creativity.adopt') continue
292
+ const id = record.payload && record.payload.hypothesisId
293
+ if (id) claimed.add(String(id))
294
+ }
295
+ const evidenced = new Set()
296
+ for (const record of readJsonl(path.join(rootDir, 'memory.jsonl'))) {
297
+ if (!record || !Array.isArray(record.tags)) continue
298
+ for (const tag of record.tags) {
299
+ if (typeof tag !== 'string' || !tag.startsWith(ADOPTION_TAG_PREFIX)) continue
300
+ const id = tag.slice(ADOPTION_TAG_PREFIX.length).trim()
301
+ if (id) evidenced.add(id)
302
+ }
303
+ }
304
+ return { claimed, evidenced }
305
+ }
306
+
260
307
  class CreativityEngine {
261
308
  constructor(dataDir, chatJson) {
309
+ this.dataDir = dataDir
262
310
  this.store = new CreativityStore(dataDir)
263
311
  this.chatJson = chatJson
264
312
  this.strategyIndex = 0
@@ -510,6 +558,20 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
510
558
  return { fermented: results.length, results, errors }
511
559
  }
512
560
 
561
+ // Fresh per call: both evidence files are append-only and a long-lived MCP
562
+ // server must see new adopt events / memory tags without a restart (same
563
+ // rationale as the insight-store mtime cache -- minus the cache, since this
564
+ // is two small file reads on a read-only report path).
565
+ _adoption() {
566
+ const { claimed, evidenced } = readAdoptionEvidence(path.dirname(this.dataDir))
567
+ const storedIds = new Set(this.store.getHypotheses().map((h) => h.id))
568
+ const adoptedIds = new Set()
569
+ for (const id of [...claimed, ...evidenced]) {
570
+ if (storedIds.has(id)) adoptedIds.add(id)
571
+ }
572
+ return { claimed, evidenced, adoptedIds }
573
+ }
574
+
513
575
  status() {
514
576
  const count = this.store.count()
515
577
  const active = this.store.getHypotheses({ status: 'active' })
@@ -518,6 +580,7 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
518
580
  // draft is a real state -- ferment() reviews draft + active -- so hiding it
519
581
  // here made `status` and `list --status draft` disagree.
520
582
  const draft = this.store.getHypotheses({ status: 'draft' })
583
+ const adoption = this._adoption()
521
584
 
522
585
  return {
523
586
  ...count,
@@ -525,6 +588,19 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
525
588
  validated: validated.length,
526
589
  rejected: rejected.length,
527
590
  draft: draft.length,
591
+ // Derived join of the two evidence axes (see readAdoptionEvidence).
592
+ // metric:false is load-bearing: this is NOT an ADR-017 evaluation metric,
593
+ // so no rate and no threshold are reported -- adoption rate was explicitly
594
+ // out of scope (D3). sources names the backing files, routeAdoption style.
595
+ adoption: {
596
+ adopted: adoption.adoptedIds.size,
597
+ claimed: adoption.claimed.size,
598
+ evidenced: adoption.evidenced.size,
599
+ metric: false,
600
+ note:
601
+ 'derived join of creativity.adopt events and hypothesis:<id> memory tags — informational; no rate or threshold by design',
602
+ sources: ADOPTION_SOURCES,
603
+ },
528
604
  recentIdeas: active.slice(-5).map(h => ({
529
605
  id: h.id,
530
606
  title: h.title,
@@ -544,6 +620,11 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
544
620
  // result. `limit: 0` (and only 0) opts back into "everything".
545
621
  list(opts = {}) {
546
622
  const limit = opts.limit === undefined ? 20 : opts.limit
623
+ // Same read-side join as status(): every row carries `adopted` so the two
624
+ // surfaces cannot disagree (the CLI/MCP entry points already share this
625
+ // engine). listRow() stays the pure store-row shape; decoration is here.
626
+ const adoption = this._adoption()
627
+ const decorate = (row) => ({ ...row, adopted: adoption.adoptedIds.has(row.id) })
547
628
  if (opts.sort === 'novelty') {
548
629
  // Relative novelty ranking (read-only, nothing written back). Pairwise
549
630
  // Jaccard is O(n²) — bound candidates to the newest 200 (spec Risk 3).
@@ -554,9 +635,13 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
554
635
  return { ...listRow(h), noveltyScore: verdict.adjustedNovelty }
555
636
  })
556
637
  rows.sort((a, b) => b.noveltyScore - a.noveltyScore || (b.createdAt || 0) - (a.createdAt || 0))
557
- return limit === 0 ? rows : rows.slice(0, limit)
638
+ const decorated = rows.map(decorate)
639
+ return limit === 0 ? decorated : decorated.slice(0, limit)
558
640
  }
559
- return this.store.getHypotheses({ status: opts.status, limit }).map(listRow)
641
+ return this.store
642
+ .getHypotheses({ status: opts.status, limit })
643
+ .map(listRow)
644
+ .map(decorate)
560
645
  }
561
646
 
562
647
  _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
@@ -605,4 +690,69 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
605
690
  }
606
691
  }
607
692
 
608
- module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf }
693
+ // Adoption evidence (idea b19bada4, design discussion 1d2e22e4 D1/D4): the
694
+ // store never gains an `adopted` status -- adoption is a relation between a
695
+ // stored hypothesis and the outside world, not an attribute of the hypothesis,
696
+ // so it lives as an append-only `creativity.adopt` trace event, optionally
697
+ // cross-checked against a memory record carrying the `hypothesis:<id>` tag.
698
+ // The host (MCP or CLI) passes its own ingestObservation so both entry points
699
+ // write byte-identical events; dataRoot is the directory holding memory.jsonl
700
+ // and traces.jsonl -- the engine sits in <dataRoot>/creativity on both hosts
701
+ // (see cliCreativityEngine in bin/mio.js). Every validation failure happens
702
+ // before ingest, so a rejected call never pollutes the event stream. This
703
+ // function writes traces only: it never touches memory.jsonl (the
704
+ // generate/ferment write nothing to Memory boundary extends here).
705
+ function adoptHypothesis(engine, args = {}, ingest, dataRoot) {
706
+ const hypothesisId = String(args.hypothesisId || '').trim()
707
+ if (!hypothesisId) {
708
+ throw new Error('mio.creativity.adopt requires hypothesisId: a stored hypothesis UUID')
709
+ }
710
+ if (!engine.store.getHypotheses().some((h) => h.id === hypothesisId)) {
711
+ throw new Error(`mio.creativity.adopt: no stored hypothesis with id "${hypothesisId}"`)
712
+ }
713
+ const memoryId = String(args.memoryId || '').trim()
714
+ if (memoryId) {
715
+ const memoryPath = path.join(dataRoot, 'memory.jsonl')
716
+ const records = fs.existsSync(memoryPath)
717
+ ? fs
718
+ .readFileSync(memoryPath, 'utf-8')
719
+ .split('\n')
720
+ .filter(Boolean)
721
+ .map((line) => {
722
+ try { return JSON.parse(line) } catch { return null }
723
+ })
724
+ .filter(Boolean)
725
+ : []
726
+ const record = records.find((r) => r && r.id === memoryId)
727
+ if (!record) {
728
+ throw new Error(`mio.creativity.adopt: no memory record with id "${memoryId}"`)
729
+ }
730
+ const tag = `hypothesis:${hypothesisId}`
731
+ if (!Array.isArray(record.tags) || !record.tags.includes(tag)) {
732
+ throw new Error(`mio.creativity.adopt: memory record "${memoryId}" is missing tag "${tag}"`)
733
+ }
734
+ }
735
+ if (typeof ingest !== 'function') {
736
+ throw new Error('mio.creativity.adopt: ingest is not available on this host')
737
+ }
738
+ const taskId = String(args.taskId || '').trim()
739
+ const note = String(args.note || '').trim()
740
+ const result = ingest({
741
+ trace_id: `creativity-adopt:${hypothesisId}:${Date.now()}`,
742
+ event_type: 'creativity.adopt',
743
+ outcome: 'success',
744
+ payload: {
745
+ hypothesisId,
746
+ ...(memoryId ? { memoryId } : {}),
747
+ ...(taskId ? { taskId } : {}),
748
+ ...(note ? { note } : {}),
749
+ },
750
+ })
751
+ return {
752
+ recorded: true,
753
+ hypothesisId,
754
+ event: (result && result.event) || result,
755
+ }
756
+ }
757
+
758
+ module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf, adoptHypothesis }
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+
5
+ // Re-export the pure TS helpers from the creativity package so CLI and MCP
6
+ // share one implementation of the A5 gate, promotion gate, and state machine.
7
+ // The JSONL store itself is the same MechanismRegistryStore the TS tests use.
8
+ let _registry = null
9
+ let _registryDir = null
10
+
11
+ function ensureRegistry(dataDir) {
12
+ if (_registry && _registryDir === dataDir) return _registry
13
+ const { MechanismRegistryStore } = require('@akemi-mio/creativity/MechanismRegistry')
14
+ const base = path.join(dataDir, 'creativity')
15
+ _registry = new MechanismRegistryStore(path.join(base, 'mechanisms.jsonl'))
16
+ _registryDir = dataDir
17
+ return _registry
18
+ }
19
+
20
+ function resetRegistryCache() {
21
+ _registry = null
22
+ _registryDir = null
23
+ }
24
+
25
+ /**
26
+ * Query the mechanism registry: active/rolled_back versions and promotion requests.
27
+ */
28
+ function mechanismRegistry(args = {}, dataDir) {
29
+ const registry = ensureRegistry(dataDir)
30
+ const mechanismId = args.mechanismId || undefined
31
+ const versions = mechanismId ? registry.listVersions(mechanismId) : registry.listVersions()
32
+ const requests = args.status ? registry.listRequests(args.status) : registry.listRequests()
33
+ return { versions, requests }
34
+ }
35
+
36
+ /**
37
+ * Submit a PromotionRequest. Gate is provided by the caller (checkSubmitGate).
38
+ * A5 is enforced inside MechanismRegistryStore.approve.
39
+ */
40
+ function mechanismPromote(args = {}, dataDir) {
41
+ const registry = ensureRegistry(dataDir)
42
+ const action = args.action || 'submit'
43
+
44
+ if (action === 'submit') {
45
+ const { checkSubmitGate } = require('@akemi-mio/creativity/MechanismRegistry')
46
+ const { DEFAULT_STOPPING_RULE } = require('@akemi-mio/creativity/ExperimentRunStore')
47
+ // Build a minimal gate from provided experimentRunIds length; full gate
48
+ // wiring is in the TS layer. For CLI/MCP thin surface we accept an
49
+ // explicit gate override or compute from runs.length vs minSamples.
50
+ const runs = (args.experimentRunIds || []).map((id, i) => ({
51
+ id,
52
+ assignment: 'candidate',
53
+ outcome: 'success',
54
+ metrics: {},
55
+ evidenceIds: [],
56
+ createdAt: Date.now() - i,
57
+ }))
58
+ const gate = args.gateOverride
59
+ ? { allowed: !!args.gateOverride.allowed, reason: args.gateOverride.reason || 'ok' }
60
+ : checkSubmitGate(DEFAULT_STOPPING_RULE, runs, args.currentSnapshotId)
61
+ const result = registry.submit(
62
+ {
63
+ mechanismId: args.mechanismId,
64
+ version: args.version,
65
+ hypothesisId: args.hypothesisId,
66
+ experimentRunIds: args.experimentRunIds,
67
+ submittedBy: args.submittedBy,
68
+ },
69
+ gate,
70
+ )
71
+ return result
72
+ }
73
+
74
+ if (action === 'approve') {
75
+ return registry.approve(args.requestId, args.approvedBy)
76
+ }
77
+
78
+ if (action === 'reject') {
79
+ return registry.reject(args.requestId, args.approvedBy, args.reason || 'rejected')
80
+ }
81
+
82
+ return { ok: false, reason: 'unknown_action' }
83
+ }
84
+
85
+ /**
86
+ * Rollback the active version of a mechanism to its rollbackTo target.
87
+ */
88
+ function mechanismRollback(args = {}, dataDir) {
89
+ const registry = ensureRegistry(dataDir)
90
+ return registry.rollback(args.mechanismId)
91
+ }
92
+
93
+ module.exports = {
94
+ mechanismRegistry,
95
+ mechanismPromote,
96
+ mechanismRollback,
97
+ ensureRegistry,
98
+ resetRegistryCache,
99
+ }
@@ -518,12 +518,23 @@ function createMemoryStore(options = {}) {
518
518
  const content = String(args.content || '').trim()
519
519
  if (!content) throw new Error('memory.record requires a non-empty content')
520
520
  const scope = normalizeScope(args.scope)
521
+ // Adoption evidence (idea b19bada4, task 2261a5f4): hypothesisId is pure
522
+ // sugar for the exact tag `mio.creativity.adopt` enforces — the record
523
+ // joins the creativity hypothesis through the same `hypothesis:<uuid>`
524
+ // tag the tags AND-filter already searches. Omitted, the record keeps
525
+ // the exact legacy shape: no extra field, tags byte-identical.
526
+ const hypothesisId = String(args.hypothesisId || '').trim()
527
+ const tags = normalizeTags(args.tags)
528
+ if (hypothesisId) {
529
+ const tag = `hypothesis:${hypothesisId}`
530
+ if (!tags.includes(tag)) tags.push(tag)
531
+ }
521
532
  const record = {
522
533
  id: createId('mem'),
523
534
  timestamp: new Date().toISOString(),
524
535
  kind: args.kind || 'note',
525
536
  content,
526
- tags: normalizeTags(args.tags),
537
+ tags,
527
538
  project: scope === 'global' ? null : args.project || projectName(),
528
539
  scope,
529
540
  source: args.source || agentId() || 'mcp',