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/README.md +32 -4
- package/bin/mio.js +214 -120
- package/package.json +3 -3
- package/server/creativity-engine.js +153 -3
- package/server/mechanism-registry.js +99 -0
- package/server/memory-store.js +12 -1
- package/server/mio-intelligence-mcp/index.js +314 -193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
37
|
+
"@akemi-mio/observer": "^0.1.5",
|
|
38
38
|
"@akemi-mio/insight": "^0.1.1",
|
|
39
|
-
"@akemi-mio/creativity": "^0.1.
|
|
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
|
-
|
|
638
|
+
const decorated = rows.map(decorate)
|
|
639
|
+
return limit === 0 ? decorated : decorated.slice(0, limit)
|
|
558
640
|
}
|
|
559
|
-
return this.store
|
|
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
|
-
|
|
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
|
+
}
|
package/server/memory-store.js
CHANGED
|
@@ -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
|
|
537
|
+
tags,
|
|
527
538
|
project: scope === 'global' ? null : args.project || projectName(),
|
|
528
539
|
scope,
|
|
529
540
|
source: args.source || agentId() || 'mcp',
|