mio-agent-runtime 0.13.5 → 0.14.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 +25 -9
- package/bin/mio.js +130 -3
- package/package.json +4 -2
- package/scripts/link-workspace.cjs +52 -0
- package/server/creativity-engine.js +49 -55
- package/server/creativity-sources.js +186 -0
- package/server/idea-generate.js +136 -0
- package/server/mio-intelligence-mcp/index.js +84 -8
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ mio creativity status Show creativity hypothesis counts and recent top id
|
|
|
57
57
|
mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
58
58
|
mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
|
|
59
59
|
mio creativity ferment Review and refine active hypotheses (calls an LLM)
|
|
60
|
+
mio idea generate --goal "..." Grounded idea pipeline: auto sources + novelty gate, persisted (calls an LLM)
|
|
60
61
|
mio insight status Insight counts: total, reported, unreported, high-value (needs @akemi-mio/insight)
|
|
61
62
|
mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
|
|
62
63
|
mio insight generate Generate insights from context (--memory "kind|content"/--summary "text"; calls an LLM)
|
|
@@ -101,7 +102,7 @@ mio --version Print the version and exit (-V)
|
|
|
101
102
|
|
|
102
103
|
## MCP 工具
|
|
103
104
|
|
|
104
|
-
MCP 服务端在 5 大域共暴露
|
|
105
|
+
MCP 服务端在 5 大域共暴露 51 个工具:
|
|
105
106
|
|
|
106
107
|
### 记忆(11)
|
|
107
108
|
`mio.memory.query` · `mio.memory.record` · `mio.memory.archive` · `mio.memory.merge` · `mio.memory.migrate` · `mio.memory.analyze` · `mio.memory.forget` · `mio.experience.list` · `mio.experience.confirm` · `mio.experience.reuse` · `mio.policy.check`
|
|
@@ -114,15 +115,15 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
114
115
|
### 洞察自省(4)
|
|
115
116
|
`mio.insight.status` · `mio.insight.list` · `mio.insight.mark_reported` · `mio.insight.generate`
|
|
116
117
|
|
|
117
|
-
### 创意引擎(
|
|
118
|
-
`mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status`
|
|
118
|
+
### 创意引擎(5)
|
|
119
|
+
`mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status` · `mio.idea.generate`
|
|
119
120
|
|
|
120
121
|
### 任务、Agent 与演化(16)
|
|
121
122
|
`mio.task.route` · `mio.task.record_outcome` · `mio.agent.register` · `mio.agent.list` · `mio.agent.report` · `mio.agent.evaluation` · `mio.host.capabilities` · `mio.evolution.status` · `mio.evolution.report` · `mio.evolution.shadow.record` · `mio.evolution.dual_write.record` · `mio.evolution.cutover.readiness` · `mio.evolution.cutover.apply` · `mio.evolution.migration.plan` · `mio.evolution.authority.plan` · `mio.phase0.report`
|
|
122
123
|
|
|
123
124
|
## 包
|
|
124
125
|
|
|
125
|
-
`mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **
|
|
126
|
+
`mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **10** 个包(声明于 `package.json`,均为 `0.1.0`)。它们仅在源码变更时才发布,所以普通的 CLI 发布不会重新发布这些包。
|
|
126
127
|
|
|
127
128
|
| 依赖 | 说明 |
|
|
128
129
|
|---|---|
|
|
@@ -135,6 +136,7 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
135
136
|
| `@akemi-mio/evolution-scheduler` | 演化调度器 |
|
|
136
137
|
| `@akemi-mio/observer` | ObserverService —— 多源数据采集、趋势分析、深度研究、世界模型、发酵、DAG 状态机、自演化引擎(唯一运行时依赖 `undici`,只用于走代理采集) |
|
|
137
138
|
| `@akemi-mio/insight` | InsightService —— 基于 LLM 的洞察生成、冲突/漂移/重复/停滞目标/摩擦检测器、在场服务、洞察打分(零 npm 依赖) |
|
|
139
|
+
| `@akemi-mio/creativity` | IdeaGenerator / NoveltyScorer / SourceAggregator —— `mio idea generate` 与创意引擎的生成算法(依赖 `@akemi-mio/core`) |
|
|
138
140
|
|
|
139
141
|
以下 `@akemi-mio` 包同样位于本 monorepo 中并独立发布,但**不是**本 CLI 的依赖——其源码从不 import 它们,因此未声明在 `package.json` 里:
|
|
140
142
|
|
|
@@ -146,9 +148,9 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
146
148
|
|
|
147
149
|
它们可直接被其它 Node 项目消费。`mio-agent-runtime` 自身是 CLI + MCP server 包。
|
|
148
150
|
|
|
149
|
-
###
|
|
151
|
+
### 曾经不发布的两个包(现已发布)
|
|
150
152
|
|
|
151
|
-
`@akemi-mio/core` 与 `@akemi-mio/creativity`
|
|
153
|
+
`@akemi-mio/core` 与 `@akemi-mio/creativity` 自 `mio-agent-runtime@0.14.0` 起发布到 npm(`0.1.0`)。`@akemi-mio/creativity` 是本 CLI 的直接依赖,`@akemi-mio/core` 经由它传递引入;其它 Node 项目可直接安装这两个包。
|
|
152
154
|
|
|
153
155
|
## 宿主说明
|
|
154
156
|
|
|
@@ -414,7 +416,7 @@ mio creativity ferment --limit 3
|
|
|
414
416
|
|
|
415
417
|
- `--source "名称|内容"` **至少两个**:引擎是把概念两两配对来产生新假设的,
|
|
416
418
|
一个来源在构造上就不可能产出组合(此时**不会**调用 LLM,直接返回
|
|
417
|
-
`need at least 2 sources
|
|
419
|
+
`need at least 2 sources`)。显式来源不足 2 个时,本地自动来源(memory / traces / 已存假设;MCP 侧还含 insight 与 observer 趋势)会先把列表补齐到 2——显式 `--source` 永远排在前面且不被替换;补齐后仍不足 2 个才报上面的错。
|
|
418
420
|
- **`--from-insights`(MCP 侧为 `fromInsights: true`)用已存洞察补足概念来源**:
|
|
419
421
|
读取 `mio insight list` 能看到的同一份存储,按分数取前 10 条映射成
|
|
420
422
|
`{ name, content, type: 'insight' }`,再与显式的 `--source` 合并。它只在
|
|
@@ -428,10 +430,24 @@ mio creativity ferment --limit 3
|
|
|
428
430
|
打印 `N pair(s) failed:`。此前 `catch {}` 让「端点挂了」与「这一对确实没有
|
|
429
431
|
新意」长得一模一样——`ideas: []` / `Nothing fermented` 两种输出都无法区分。
|
|
430
432
|
|
|
433
|
+
### 目标驱动的 idea 流水线(`mio idea generate`)
|
|
434
|
+
|
|
435
|
+
一个 goal 直接产出可实验的假设(MCP 面为 `mio.idea.generate`),来源全程本地优先组装:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
mio idea generate --goal "降低 MCP 调用延迟" --context "近三天 p95 上升" --constraint "不引入新依赖"
|
|
439
|
+
mio idea generate --goal "扩大命令覆盖" --json
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **来源顺序**:goal → context → constraints → 记忆 grounding(取前 3 条,`scope: all`)→ 本地自动来源(memory / traces / 已存假设,MCP 侧还含 insight 与 observer 趋势),按 `origin` 去重;凑不足 2 个时返回 `reason` 文案而不是报错。
|
|
443
|
+
- **每条 idea 都持久化**到 `<MIO_HOME>/creativity/creativity-hypotheses.jsonl`(`draft`,带 `provenance`:strategy / technique / relatedMemoryIds / 命中来源 / generatedAt),**绝不写入 Mio memory**。
|
|
444
|
+
- **novelty 门禁**:与已 `rejected` 假设近似重复的候选落盘为 `rejected` 并带 `rejectionReason`;与近期假设相似的只降分。拒绝的假设同样入库,供后续去重。
|
|
445
|
+
- `--num` 钳制在 1..3(默认 3);`--json` 返回完整结构 `{ ideas, generatedAt, groundedWith, persistedIds }`。
|
|
446
|
+
|
|
431
447
|
#### LLM 配置
|
|
432
448
|
|
|
433
|
-
只有
|
|
434
|
-
`mio insight generate`、`mio observer ferment`。它们共用
|
|
449
|
+
只有 5 条命令会调用大模型:`mio creativity generate` / `ferment`、
|
|
450
|
+
`mio idea generate`、`mio insight generate`、`mio observer ferment`。它们共用
|
|
435
451
|
`server/llm-client.js` 这一份实现,配置也共用同一套解析顺序:
|
|
436
452
|
|
|
437
453
|
```
|
package/bin/mio.js
CHANGED
|
@@ -24,6 +24,8 @@ const { createMemoryStore } = require('../server/memory-store.js')
|
|
|
24
24
|
const { createExperienceStore } = require('../server/experience-store.js')
|
|
25
25
|
const { createPolicyStore } = require('../server/policy-store.js')
|
|
26
26
|
const { CreativityEngine, sourcesFromInsights } = require('../server/creativity-engine.js')
|
|
27
|
+
const { buildAutoSources, topUpSources } = require('../server/creativity-sources.js')
|
|
28
|
+
const { runIdeaGenerate, groundingFrom } = require('../server/idea-generate.js')
|
|
27
29
|
const { createAgentStore } = require('../server/agent-store.js')
|
|
28
30
|
const { createEvaluationStore } = require('../server/evaluation-store.js')
|
|
29
31
|
const { createInsightStore, isInsightAvailable } = require('../server/insight-store.js')
|
|
@@ -2065,7 +2067,7 @@ function observerCommand(args, useJson) {
|
|
|
2065
2067
|
function creativityUsage(write = console.log) {
|
|
2066
2068
|
write(`Usage:
|
|
2067
2069
|
mio creativity status Show hypothesis counts and recent top ideas
|
|
2068
|
-
mio creativity list List hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
2070
|
+
mio creativity list List hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
|
|
2069
2071
|
|
|
2070
2072
|
mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
|
|
2071
2073
|
mio creativity ferment Review/refine active hypotheses (calls an LLM)
|
|
@@ -2073,6 +2075,7 @@ function creativityUsage(write = console.log) {
|
|
|
2073
2075
|
Options:
|
|
2074
2076
|
--status name Filter list by status
|
|
2075
2077
|
--limit N Max results for list / ferment (default 20 / 5); --limit 0 lists everything
|
|
2078
|
+
--sort novelty Rank list by relative noveltyScore (a dedup signal, not a quality score)
|
|
2076
2079
|
--source "name|content" A concept source for generate; repeat 2+ times
|
|
2077
2080
|
--from-insights Seed generate's sources from stored insights (needs @akemi-mio/insight)
|
|
2078
2081
|
--strategy explore|signal|stable Generation strategy (auto-selected if omitted)
|
|
@@ -2081,6 +2084,7 @@ Options:
|
|
|
2081
2084
|
Examples:
|
|
2082
2085
|
mio creativity status
|
|
2083
2086
|
mio creativity list --status rejected --limit 10
|
|
2087
|
+
mio creativity list --sort novelty
|
|
2084
2088
|
mio creativity generate --source "auth|token rotation" --source "cache|write-through"
|
|
2085
2089
|
mio creativity generate --from-insights --source "auth|token rotation"
|
|
2086
2090
|
|
|
@@ -2114,7 +2118,7 @@ function printCreativityList(items) {
|
|
|
2114
2118
|
items.forEach((h, index) => {
|
|
2115
2119
|
const when = h.createdAt ? new Date(h.createdAt).toISOString().slice(0, 19).replace('T', ' ') : ''
|
|
2116
2120
|
const labels = Array.isArray(h.sourceLabels) && h.sourceLabels.length > 0 ? h.sourceLabels.join(' + ') : '—'
|
|
2117
|
-
console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score})`)
|
|
2121
|
+
console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score}${h.noveltyScore !== undefined ? ` noveltyScore=${h.noveltyScore}` : ''})`)
|
|
2118
2122
|
console.log(` ${labels} | ${when} | ${h.id}`)
|
|
2119
2123
|
if (h.fermentCount) console.log(` fermented ${h.fermentCount}x`)
|
|
2120
2124
|
})
|
|
@@ -2162,6 +2166,26 @@ function printCreativityErrors(errors, label) {
|
|
|
2162
2166
|
}
|
|
2163
2167
|
}
|
|
2164
2168
|
|
|
2169
|
+
// Auto sources for generate's top-up: the same local providers the MCP server
|
|
2170
|
+
// uses, assembled from the CLI's global MIO_HOME (spec Capability 2). Every
|
|
2171
|
+
// layer degrades to [] — a missing optional package or an unreadable store
|
|
2172
|
+
// must never fail `mio creativity generate`.
|
|
2173
|
+
function cliAutoSources() {
|
|
2174
|
+
let insights = []
|
|
2175
|
+
let trends = []
|
|
2176
|
+
try {
|
|
2177
|
+
if (isInsightAvailable()) insights = cliInsightStore().list({}) || []
|
|
2178
|
+
} catch (_) {}
|
|
2179
|
+
try {
|
|
2180
|
+
if (isObserverAvailable()) trends = cliObserverStore().trends({ limit: 3 }) || []
|
|
2181
|
+
} catch (_) {}
|
|
2182
|
+
try {
|
|
2183
|
+
return buildAutoSources({ dataDir: MIO_HOME, insights, trends })
|
|
2184
|
+
} catch (_) {
|
|
2185
|
+
return []
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2165
2189
|
function creativityGenerateCommand(args, useJson) {
|
|
2166
2190
|
const flags = args.slice(2)
|
|
2167
2191
|
let sources = parseSources(flags)
|
|
@@ -2191,6 +2215,15 @@ function creativityGenerateCommand(args, useJson) {
|
|
|
2191
2215
|
sources = sources.concat(seeds)
|
|
2192
2216
|
}
|
|
2193
2217
|
|
|
2218
|
+
// Auto top-up (spec Capability 2, injection point 2): explicit --source
|
|
2219
|
+
// entries win; when fewer than two remain, local memory/traces/stored
|
|
2220
|
+
// hypotheses (plus insight/observer observations) fill the gap. If the
|
|
2221
|
+
// topped-up set is still short, the errors below keep their old wording.
|
|
2222
|
+
if (sources.length < 2) {
|
|
2223
|
+
const topped = topUpSources(sources, cliAutoSources())
|
|
2224
|
+
if (topped.length >= 2) sources = topped
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2194
2227
|
if (sources.length < 2) {
|
|
2195
2228
|
if (flagPresent(flags, '--from-insights')) {
|
|
2196
2229
|
console.error(`mio creativity generate needs at least 2 concept sources; --from-insights only supplied ${sources.length}.`)
|
|
@@ -2263,6 +2296,90 @@ function creativityFermentCommand(args, useJson) {
|
|
|
2263
2296
|
})
|
|
2264
2297
|
}
|
|
2265
2298
|
|
|
2299
|
+
function ideaUsage() {
|
|
2300
|
+
console.error('usage: mio idea generate --goal "one line" [--context "..."] [--constraint "..."] [--num N] [--json]')
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
async function ideaGenerateCommand(args, useJson) {
|
|
2304
|
+
const flags = args.slice(2)
|
|
2305
|
+
const goal = (optionValue(flags, '--goal') || '').trim()
|
|
2306
|
+
if (!goal) {
|
|
2307
|
+
console.error('mio idea generate requires --goal "..."')
|
|
2308
|
+
ideaUsage()
|
|
2309
|
+
process.exitCode = 1
|
|
2310
|
+
return
|
|
2311
|
+
}
|
|
2312
|
+
const context = optionValue(flags, '--context') || ''
|
|
2313
|
+
const constraints = optionValues(flags, '--constraint')
|
|
2314
|
+
let numIdeas
|
|
2315
|
+
try {
|
|
2316
|
+
numIdeas = parseNumberOption(flags, '--num')
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
console.error(error.message || error)
|
|
2319
|
+
ideaUsage()
|
|
2320
|
+
process.exitCode = 1
|
|
2321
|
+
return
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// Grounding from the CLI's global MIO_HOME. A failed query degrades to "no
|
|
2325
|
+
// grounding" — never a hard error (spec Capability 1: local-first, tolerant).
|
|
2326
|
+
// The core logger echoes every event to stdout; in --json mode that would
|
|
2327
|
+
// corrupt the payload jsonOrText must own, so the echo is routed to stderr
|
|
2328
|
+
// while the pipeline runs (executed deviation — the plan assumed pure JSON).
|
|
2329
|
+
const echoLog = useJson ? console.log : null
|
|
2330
|
+
if (useJson) console.log = (...params) => console.error(...params)
|
|
2331
|
+
|
|
2332
|
+
let result
|
|
2333
|
+
try {
|
|
2334
|
+
let grounding = { sources: [], relatedMemoryIds: [], groundedWith: 0 }
|
|
2335
|
+
try {
|
|
2336
|
+
const qr = await cliMemoryStore().queryMemory({ query: goal, limit: 3, scope: 'all' })
|
|
2337
|
+
grounding = groundingFrom(qr)
|
|
2338
|
+
} catch (_) {}
|
|
2339
|
+
|
|
2340
|
+
if (!useJson) warnIfLlmUnconfigured()
|
|
2341
|
+
|
|
2342
|
+
result = await runIdeaGenerate({
|
|
2343
|
+
goal,
|
|
2344
|
+
context,
|
|
2345
|
+
constraints,
|
|
2346
|
+
numIdeas,
|
|
2347
|
+
chatJson,
|
|
2348
|
+
store: cliCreativityEngine().store,
|
|
2349
|
+
groundingSources: grounding.sources,
|
|
2350
|
+
relatedMemoryIds: grounding.relatedMemoryIds,
|
|
2351
|
+
autoSources: cliAutoSources(),
|
|
2352
|
+
})
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
console.error(error.message || error)
|
|
2355
|
+
process.exitCode = 1
|
|
2356
|
+
return
|
|
2357
|
+
} finally {
|
|
2358
|
+
if (useJson) console.log = echoLog
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (useJson) return jsonOrText(result, true)
|
|
2362
|
+
if (result.reason) {
|
|
2363
|
+
console.log(`No ideas generated: ${result.reason}`)
|
|
2364
|
+
return
|
|
2365
|
+
}
|
|
2366
|
+
console.log(`Generated ${result.ideas.length} idea(s) (grounded with ${result.groundedWith} memories)`)
|
|
2367
|
+
result.ideas.forEach((idea) => {
|
|
2368
|
+
console.log(`- [${idea.hypothesis.status}] ${idea.title}`)
|
|
2369
|
+
console.log(` novelty=${idea.hypothesis.novelty} feasibility=${idea.hypothesis.feasibility} impact=${idea.hypothesis.impact} | ${idea.hypothesis.id}`)
|
|
2370
|
+
if (idea.provenance && idea.provenance.technique) console.log(` technique=${idea.provenance.technique}`)
|
|
2371
|
+
if (idea.hypothesis.rejectionReason) console.log(` rejected: ${idea.hypothesis.rejectionReason}`)
|
|
2372
|
+
})
|
|
2373
|
+
console.log(`Persisted ${result.persistedIds.length} idea(s)`)
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
function ideaCommand(args, useJson) {
|
|
2377
|
+
const sub = args[1]
|
|
2378
|
+
if (sub === 'generate') return ideaGenerateCommand(args, useJson)
|
|
2379
|
+
ideaUsage()
|
|
2380
|
+
process.exitCode = 1
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2266
2383
|
function creativityCommand(args, useJson) {
|
|
2267
2384
|
const sub = args[1]
|
|
2268
2385
|
|
|
@@ -2289,9 +2406,16 @@ function creativityCommand(args, useJson) {
|
|
|
2289
2406
|
if (sub === 'status') {
|
|
2290
2407
|
result = engine.status()
|
|
2291
2408
|
} else {
|
|
2409
|
+
const sort = optionValue(flags, '--sort')
|
|
2410
|
+
if (sort !== undefined && sort !== 'novelty') {
|
|
2411
|
+
console.error(`mio creativity list: unknown --sort "${sort}" (supported: novelty)`)
|
|
2412
|
+
process.exitCode = 1
|
|
2413
|
+
return
|
|
2414
|
+
}
|
|
2292
2415
|
result = engine.list({
|
|
2293
2416
|
status: optionValue(flags, '--status'),
|
|
2294
2417
|
limit: parseNumberOption(flags, '--limit'),
|
|
2418
|
+
sort,
|
|
2295
2419
|
})
|
|
2296
2420
|
}
|
|
2297
2421
|
} catch (error) {
|
|
@@ -3369,9 +3493,10 @@ Usage:
|
|
|
3369
3493
|
mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
|
|
3370
3494
|
mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
|
|
3371
3495
|
mio creativity status Show creativity hypothesis counts and recent top ideas
|
|
3372
|
-
mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
3496
|
+
mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
|
|
3373
3497
|
mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
|
|
3374
3498
|
mio creativity ferment Review and refine active hypotheses (calls an LLM)
|
|
3499
|
+
mio idea generate --goal "..." Grounded idea pipeline: auto sources, novelty gate, persisted (calls an LLM)
|
|
3375
3500
|
mio insight status Insight counts: total, reported, unreported, high-value
|
|
3376
3501
|
mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
|
|
3377
3502
|
mio insight generate Generate insights from context (--memory "kind|content"/--summary "text"; calls an LLM)
|
|
@@ -3435,6 +3560,8 @@ async function main() {
|
|
|
3435
3560
|
return policyCommand(args, useJson)
|
|
3436
3561
|
case 'creativity':
|
|
3437
3562
|
return creativityCommand(args, useJson)
|
|
3563
|
+
case 'idea':
|
|
3564
|
+
return ideaCommand(args, useJson)
|
|
3438
3565
|
case 'insight':
|
|
3439
3566
|
return insightCommand(args, useJson)
|
|
3440
3567
|
case 'observer':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"!server/mio-intelligence-mcp/__tests__"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
+
"pretest": "node scripts/link-workspace.cjs",
|
|
20
21
|
"check": "node scripts/check-syntax.cjs",
|
|
21
22
|
"test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
|
|
22
23
|
"prepack": "npm run check",
|
|
@@ -34,7 +35,8 @@
|
|
|
34
35
|
"@akemi-mio/evolution-safety": "^0.1.0",
|
|
35
36
|
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
36
37
|
"@akemi-mio/observer": "^0.1.4",
|
|
37
|
-
"@akemi-mio/insight": "^0.1.1"
|
|
38
|
+
"@akemi-mio/insight": "^0.1.1",
|
|
39
|
+
"@akemi-mio/creativity": "^0.1.0"
|
|
38
40
|
},
|
|
39
41
|
"main": "index.js",
|
|
40
42
|
"directories": {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Creates the node_modules junctions mio-cli needs to require the workspace
|
|
5
|
+
// packages it depends on at runtime (@akemi-mio/insight etc. already exist as
|
|
6
|
+
// junctions in a normal checkout; a fresh clone has none). Idempotent: an
|
|
7
|
+
// existing link (or directory) is left alone. Windows junctions, because this
|
|
8
|
+
// checkout lives on NTFS and junctions need no admin rights.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
const REPO = path.resolve(__dirname, '..', '..', '..')
|
|
14
|
+
const links = [
|
|
15
|
+
{
|
|
16
|
+
at: path.join(REPO, 'packages', 'mio-cli', 'node_modules', '@akemi-mio', 'creativity'),
|
|
17
|
+
target: path.join(REPO, 'packages', 'creativity'),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
at: path.join(REPO, 'packages', 'creativity', 'node_modules', '@akemi-mio', 'core'),
|
|
21
|
+
target: path.join(REPO, 'packages', 'core'),
|
|
22
|
+
},
|
|
23
|
+
// Root-level links: the new tests under tests/main/creativity/ import
|
|
24
|
+
// '@akemi-mio/creativity/*' from the repo root, and the lockfile's
|
|
25
|
+
// link:true entries are not materialized on disk (ground rule 4 forbids
|
|
26
|
+
// the `npm install` that would create them).
|
|
27
|
+
{
|
|
28
|
+
at: path.join(REPO, 'node_modules', '@akemi-mio', 'creativity'),
|
|
29
|
+
target: path.join(REPO, 'packages', 'creativity'),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
at: path.join(REPO, 'node_modules', '@akemi-mio', 'core'),
|
|
33
|
+
target: path.join(REPO, 'packages', 'core'),
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for (const { at, target } of links) {
|
|
38
|
+
let exists = false
|
|
39
|
+
try {
|
|
40
|
+
fs.lstatSync(at)
|
|
41
|
+
exists = true
|
|
42
|
+
} catch (_) {
|
|
43
|
+
exists = false
|
|
44
|
+
}
|
|
45
|
+
if (exists) {
|
|
46
|
+
console.log(`exists ${at}`)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(path.dirname(at), { recursive: true })
|
|
50
|
+
fs.symlinkSync(target, at, 'junction')
|
|
51
|
+
console.log(`linked ${at} -> ${target}`)
|
|
52
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
|
+
const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
|
|
6
7
|
|
|
7
8
|
// Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
|
|
8
9
|
// guard server/insight-store.js uses (commit 0ba7fa8). Every getter below used
|
|
@@ -27,6 +28,13 @@ function clamp100(value, fallback = 50) {
|
|
|
27
28
|
return Math.min(100, Math.max(0, n))
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// ISO timestamp for provenance: accepts epoch ms or date strings, returns
|
|
32
|
+
// undefined when the value is missing or unparsable.
|
|
33
|
+
function isoOf(value) {
|
|
34
|
+
const t = typeof value === 'number' ? value : Date.parse(value || '')
|
|
35
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
// ═══════════════════════════════════════════════
|
|
31
39
|
// JSONL Store — ideas, combos, experiments
|
|
32
40
|
// ═══════════════════════════════════════════════
|
|
@@ -173,43 +181,6 @@ function mixConcepts(sources, exploredPairs, rng) {
|
|
|
173
181
|
return pairs.slice(0, Math.min(5, pairs.length))
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
// ═══════════════════════════════════════════════
|
|
177
|
-
// Novelty Scorer — reject duplicates
|
|
178
|
-
// ═══════════════════════════════════════════════
|
|
179
|
-
|
|
180
|
-
function bigramSet(text) {
|
|
181
|
-
const s = new Set()
|
|
182
|
-
const lower = text.toLowerCase()
|
|
183
|
-
for (let i = 0; i < lower.length - 1; i++) {
|
|
184
|
-
s.add(lower.slice(i, i + 2))
|
|
185
|
-
}
|
|
186
|
-
return s
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function jaccard(a, b) {
|
|
190
|
-
if (a.size === 0 && b.size === 0) return 1
|
|
191
|
-
let inter = 0
|
|
192
|
-
for (const x of a) if (b.has(x)) inter++
|
|
193
|
-
return inter / (a.size + b.size - inter)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function evaluateNovelty(idea, recent, rejected) {
|
|
197
|
-
const ideaBigrams = bigramSet(idea.title + ' ' + idea.idea)
|
|
198
|
-
for (const r of recent) {
|
|
199
|
-
const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
|
|
200
|
-
if (sim > 0.7) {
|
|
201
|
-
return { shouldReject: true, rejectReason: `too similar to recent: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
for (const r of rejected) {
|
|
205
|
-
const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
|
|
206
|
-
if (sim > 0.6) {
|
|
207
|
-
return { shouldReject: true, rejectReason: `matches rejected: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return { shouldReject: false, adjustedNovelty: Math.min(100, idea.novelty + 5) }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
184
|
// ═══════════════════════════════════════════════
|
|
214
185
|
// Creativity Engine — main orchestrator
|
|
215
186
|
// ═══════════════════════════════════════════════
|
|
@@ -234,12 +205,38 @@ function sourcesFromInsights(insights, limit = 10) {
|
|
|
234
205
|
Array.isArray(i.evidence) && i.evidence.length > 0 ? `Evidence: ${i.evidence.join('; ')}` : '',
|
|
235
206
|
].filter(Boolean).join('\n'),
|
|
236
207
|
type: 'insight',
|
|
237
|
-
|
|
208
|
+
// > 0.7: IdeaGenerator.applyTemperature keeps any weight>0.7 source unconditionally.
|
|
209
|
+
weight: 0.78,
|
|
210
|
+
origin: `insight:${i.id || i.title || 'unknown'}`,
|
|
211
|
+
...(i.id ? { originId: i.id } : {}),
|
|
212
|
+
...(isoOf(i.createdAt) ? { timestamp: isoOf(i.createdAt) } : {}),
|
|
238
213
|
}))
|
|
239
214
|
}
|
|
240
215
|
|
|
241
216
|
const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
|
|
242
217
|
|
|
218
|
+
// One row shape for every list variant — the fields JSON/CLI consumers already
|
|
219
|
+
// depend on. `noveltyScore` is added only by sort=novelty (relative ranking),
|
|
220
|
+
// never confused with the quality sum `score`.
|
|
221
|
+
function listRow(h) {
|
|
222
|
+
return {
|
|
223
|
+
id: h.id,
|
|
224
|
+
title: h.title,
|
|
225
|
+
idea: h.idea,
|
|
226
|
+
status: h.status,
|
|
227
|
+
novelty: h.novelty,
|
|
228
|
+
feasibility: h.feasibility,
|
|
229
|
+
impact: h.impact,
|
|
230
|
+
...(h.logic !== undefined ? { logic: h.logic } : {}),
|
|
231
|
+
score: h.novelty + h.feasibility + h.impact,
|
|
232
|
+
sourceLabels: h.sourceLabels,
|
|
233
|
+
createdAt: h.createdAt,
|
|
234
|
+
fermentedAt: h.fermentedAt,
|
|
235
|
+
fermentCount: h.fermentCount,
|
|
236
|
+
...(h.mergedInto !== undefined ? { mergedInto: h.mergedInto } : {}),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
243
240
|
class CreativityEngine {
|
|
244
241
|
constructor(dataDir, chatJson) {
|
|
245
242
|
this.store = new CreativityStore(dataDir)
|
|
@@ -506,22 +503,19 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
|
|
|
506
503
|
// result. `limit: 0` (and only 0) opts back into "everything".
|
|
507
504
|
list(opts = {}) {
|
|
508
505
|
const limit = opts.limit === undefined ? 20 : opts.limit
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
fermentCount: h.fermentCount,
|
|
523
|
-
...(h.mergedInto !== undefined ? { mergedInto: h.mergedInto } : {}),
|
|
524
|
-
}))
|
|
506
|
+
if (opts.sort === 'novelty') {
|
|
507
|
+
// Relative novelty ranking (read-only, nothing written back). Pairwise
|
|
508
|
+
// Jaccard is O(n²) — bound candidates to the newest 200 (spec Risk 3).
|
|
509
|
+
const candidates = this.store.getHypotheses({ status: opts.status }).slice(-200)
|
|
510
|
+
const rows = candidates.map((h) => {
|
|
511
|
+
const others = candidates.filter((x) => x.id !== h.id)
|
|
512
|
+
const verdict = evaluateNovelty(h, others, [])
|
|
513
|
+
return { ...listRow(h), noveltyScore: verdict.adjustedNovelty }
|
|
514
|
+
})
|
|
515
|
+
rows.sort((a, b) => b.noveltyScore - a.noveltyScore || (b.createdAt || 0) - (a.createdAt || 0))
|
|
516
|
+
return limit === 0 ? rows : rows.slice(0, limit)
|
|
517
|
+
}
|
|
518
|
+
return this.store.getHypotheses({ status: opts.status, limit }).map(listRow)
|
|
525
519
|
}
|
|
526
520
|
|
|
527
521
|
_buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
|
|
@@ -570,4 +564,4 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
|
|
|
570
564
|
}
|
|
571
565
|
}
|
|
572
566
|
|
|
573
|
-
module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights }
|
|
567
|
+
module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights, isoOf }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Source auto-aggregation for the creativity engine (spec Capability 2).
|
|
4
|
+
//
|
|
5
|
+
// Default providers read ONLY local MIO_HOME data — zero network. Insight and
|
|
6
|
+
// observer sources arrive as plain arrays from the caller, because only the
|
|
7
|
+
// entry point knows which store is authoritative (MIO_HOME for the CLI, the
|
|
8
|
+
// MCP server's MIO_DATA_DIR) and whether its optional package resolves.
|
|
9
|
+
// SourceAggregator already tolerates a throwing provider (per-provider
|
|
10
|
+
// try/catch with a WARN log), so providers here do not need extra guards.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const { SourceAggregator } = require('@akemi-mio/creativity/SourceAggregator')
|
|
15
|
+
const { sourcesFromInsights, isoOf } = require('./creativity-engine.js')
|
|
16
|
+
|
|
17
|
+
function readJsonl(file) {
|
|
18
|
+
try {
|
|
19
|
+
return fs
|
|
20
|
+
.readFileSync(file, 'utf8')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((line) => {
|
|
24
|
+
try { return JSON.parse(line) } catch { return null }
|
|
25
|
+
})
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Every weight is > 0.7 on purpose: IdeaGenerator.applyTemperature keeps any
|
|
33
|
+
// source with weight > 0.7 unconditionally (temperature 0.5 middle branch), so
|
|
34
|
+
// the assembled set reaches the prompt intact — deterministic, no RNG survival.
|
|
35
|
+
const WEIGHTS = {
|
|
36
|
+
memory: 0.75,
|
|
37
|
+
traceOk: 0.74,
|
|
38
|
+
traceFail: 0.73,
|
|
39
|
+
hypothesisActive: 0.72,
|
|
40
|
+
hypothesisRejected: 0.72,
|
|
41
|
+
trend: 0.76,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function memoryProvider(dataDir) {
|
|
45
|
+
return {
|
|
46
|
+
domain: 'feedback',
|
|
47
|
+
name: 'memory',
|
|
48
|
+
collect() {
|
|
49
|
+
return readJsonl(path.join(dataDir, 'memory.jsonl'))
|
|
50
|
+
.filter((r) => r && r.archived !== true && (r.kind === 'decision' || r.kind === 'note'))
|
|
51
|
+
.slice(-6)
|
|
52
|
+
.map((r) => ({
|
|
53
|
+
name: `${r.kind}: ${String(r.content || '').slice(0, 40)}`,
|
|
54
|
+
content: String(r.content || ''),
|
|
55
|
+
type: 'feedback',
|
|
56
|
+
weight: WEIGHTS.memory,
|
|
57
|
+
origin: `memory:${r.id}`,
|
|
58
|
+
originId: r.id,
|
|
59
|
+
timestamp: isoOf(r.timestamp),
|
|
60
|
+
}))
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function traceProviders(dataDir) {
|
|
66
|
+
const events = () => readJsonl(path.join(dataDir, 'traces.jsonl')).slice(-40)
|
|
67
|
+
const fmt = (r) => `${r.event_type}${r.outcome ? `/${r.outcome}` : ''}: ${JSON.stringify(r.payload || {}).slice(0, 300)}`
|
|
68
|
+
const stamp = (r) => isoOf(r.timestamp ?? r.ts)
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
domain: 'behavior',
|
|
72
|
+
name: 'traces-success',
|
|
73
|
+
collect() {
|
|
74
|
+
return events()
|
|
75
|
+
.filter((r) => r.event_type === 'task_outcome' && r.outcome === 'success')
|
|
76
|
+
.slice(-3)
|
|
77
|
+
.map((r) => ({
|
|
78
|
+
name: `success: ${String((r.payload && (r.payload.task || r.payload.summary)) || r.trace_id || 'task').slice(0, 40)}`,
|
|
79
|
+
content: fmt(r),
|
|
80
|
+
type: 'behavior',
|
|
81
|
+
weight: WEIGHTS.traceOk,
|
|
82
|
+
origin: `trace:${r.trace_id}`,
|
|
83
|
+
originId: r.trace_id,
|
|
84
|
+
timestamp: stamp(r),
|
|
85
|
+
}))
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
domain: 'failure',
|
|
90
|
+
name: 'traces-failure',
|
|
91
|
+
collect() {
|
|
92
|
+
return events()
|
|
93
|
+
.filter(
|
|
94
|
+
(r) =>
|
|
95
|
+
(r.event_type === 'error' || r.event_type === 'retry') ||
|
|
96
|
+
(r.event_type === 'task_outcome' && (r.outcome === 'failure' || r.outcome === 'error' || r.outcome === 'aborted')),
|
|
97
|
+
)
|
|
98
|
+
.slice(-3)
|
|
99
|
+
.map((r) => ({
|
|
100
|
+
name: `failure: ${String(r.event_type)}${r.trace_id ? ` ${String(r.trace_id).slice(0, 12)}` : ''}`,
|
|
101
|
+
content: fmt(r),
|
|
102
|
+
type: 'failure',
|
|
103
|
+
weight: WEIGHTS.traceFail,
|
|
104
|
+
origin: `trace:${r.trace_id || 'unknown'}`,
|
|
105
|
+
originId: r.trace_id,
|
|
106
|
+
timestamp: stamp(r),
|
|
107
|
+
}))
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hypothesesProvider(dataDir) {
|
|
114
|
+
return {
|
|
115
|
+
domain: 'module',
|
|
116
|
+
name: 'hypotheses',
|
|
117
|
+
collect() {
|
|
118
|
+
return readJsonl(path.join(dataDir, 'creativity', 'creativity-hypotheses.jsonl'))
|
|
119
|
+
.filter((h) => h && (h.status === 'active' || h.status === 'rejected'))
|
|
120
|
+
.slice(-6)
|
|
121
|
+
.map((h) => ({
|
|
122
|
+
name: String(h.title || h.id).slice(0, 60),
|
|
123
|
+
content: `idea: ${h.idea}\nrisk: ${h.risk || '-'}`,
|
|
124
|
+
type: h.status === 'rejected' ? 'failure' : 'knowledge',
|
|
125
|
+
weight: h.status === 'rejected' ? WEIGHTS.hypothesisRejected : WEIGHTS.hypothesisActive,
|
|
126
|
+
origin: `hypothesis:${h.id}`,
|
|
127
|
+
originId: h.id,
|
|
128
|
+
timestamp: isoOf(h.createdAt),
|
|
129
|
+
}))
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Observer trend reports → provocation sources. The report schema has drifted
|
|
135
|
+
// across observer versions, so fields are read defensively with fallbacks.
|
|
136
|
+
function trendSources(trends) {
|
|
137
|
+
return (Array.isArray(trends) ? trends : []).slice(0, 4).map((t, i) => {
|
|
138
|
+
const title = (t && (t.title || t.name || t.date)) || `trend-${i}`
|
|
139
|
+
const body =
|
|
140
|
+
(t && t.summary) ||
|
|
141
|
+
(Array.isArray(t && t.themes) ? t.themes.join('; ') : '') ||
|
|
142
|
+
JSON.stringify(t || {}).slice(0, 400)
|
|
143
|
+
const key = (t && (t.date || t.id)) || String(i)
|
|
144
|
+
const stamp = isoOf(t && (t.generatedAt || t.date))
|
|
145
|
+
return {
|
|
146
|
+
name: String(title).slice(0, 60),
|
|
147
|
+
content: String(body),
|
|
148
|
+
type: 'provocation',
|
|
149
|
+
weight: WEIGHTS.trend,
|
|
150
|
+
origin: `observer-trends:${key}`,
|
|
151
|
+
...(t && t.id ? { originId: t.id } : {}),
|
|
152
|
+
...(stamp ? { timestamp: stamp } : {}),
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Assemble every local (+ optionally insight/observer) domain through the
|
|
158
|
+
// package SourceAggregator so dedup, per-domain caps and failure tolerance
|
|
159
|
+
// behave identically for idea.generate and the generate top-up.
|
|
160
|
+
function buildAutoSources({ dataDir, insights = [], trends = [] }) {
|
|
161
|
+
const aggregator = new SourceAggregator(4, 1)
|
|
162
|
+
aggregator.addProvider(memoryProvider(dataDir))
|
|
163
|
+
for (const provider of traceProviders(dataDir)) aggregator.addProvider(provider)
|
|
164
|
+
aggregator.addProvider(hypothesesProvider(dataDir))
|
|
165
|
+
if (Array.isArray(insights) && insights.length > 0) {
|
|
166
|
+
aggregator.addProvider({ domain: 'observation', name: 'insight', collect: () => sourcesFromInsights(insights) })
|
|
167
|
+
}
|
|
168
|
+
const trendList = Array.isArray(trends) ? trends : []
|
|
169
|
+
if (trendList.length > 0) {
|
|
170
|
+
aggregator.addProvider({ domain: 'external', name: 'observer-trends', collect: () => trendSources(trendList) })
|
|
171
|
+
}
|
|
172
|
+
return aggregator.build()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Explicit --source entries always win: auto sources only top a set up to the
|
|
176
|
+
// two entries the concept mixer needs; they never replace or reorder explicit
|
|
177
|
+
// ones (spec Capability 2, injection point 2).
|
|
178
|
+
function topUpSources(explicit, auto) {
|
|
179
|
+
const list = Array.isArray(explicit) ? explicit.filter(Boolean) : []
|
|
180
|
+
if (list.length >= 2) return list
|
|
181
|
+
const seen = new Set(list.map((s) => s.name))
|
|
182
|
+
const extra = (Array.isArray(auto) ? auto : []).filter((s) => s && !seen.has(s.name))
|
|
183
|
+
return [...list, ...extra]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { buildAutoSources, topUpSources, trendSources, memoryProvider, traceProviders, hypothesesProvider }
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// `mio.idea.generate` — one-shot goal-driven idea pipeline (spec Capability 1).
|
|
4
|
+
// Shared by the CLI (`mio idea generate`) and the MCP server (tool
|
|
5
|
+
// `mio.idea.generate`): each entry point only assembles entry-specific inputs
|
|
6
|
+
// (which store is authoritative, where local data lives) and renders the
|
|
7
|
+
// response this module returns.
|
|
8
|
+
//
|
|
9
|
+
// Contract:
|
|
10
|
+
// - source order: goal → context → constraints → grounding → local auto,
|
|
11
|
+
// deduplicated by `origin` (fallback name), first occurrence wins — so a
|
|
12
|
+
// memory that arrives both as a grounding hit and through the local
|
|
13
|
+
// provider contributes exactly once, the grounding copy first;
|
|
14
|
+
// - pairing strategy: cross-type `explore` only when two types exist, else
|
|
15
|
+
// the `stable` fallback (an all-knowledge set explores into ZERO combos);
|
|
16
|
+
// - every candidate passes evaluateNovelty against stored hypotheses: a
|
|
17
|
+
// near-duplicate of a REJECTED one is persisted as rejected with a reason,
|
|
18
|
+
// a merely-similar recent one is down-scored; drafts AND rejects both land
|
|
19
|
+
// in the store so later runs can dedup against them;
|
|
20
|
+
// - ideas are never written to Mio memory — CreativityStore only.
|
|
21
|
+
|
|
22
|
+
const { IdeaGenerator } = require('@akemi-mio/creativity/IdeaGenerator')
|
|
23
|
+
const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
|
|
24
|
+
|
|
25
|
+
// All > 0.7: IdeaGenerator.applyTemperature keeps such sources unconditionally
|
|
26
|
+
// (temperature 0.5 middle branch) — deterministic prompt, no RNG survival.
|
|
27
|
+
const WEIGHTS = { goal: 0.95, context: 0.9, constraint: 0.88, grounding: 0.75 }
|
|
28
|
+
|
|
29
|
+
// `queryMemory` result → grounding sources + ids. Tolerates null/empty
|
|
30
|
+
// (a thrown query is the caller's problem to catch; a miss is not).
|
|
31
|
+
function groundingFrom(queryResult) {
|
|
32
|
+
const results = Array.isArray(queryResult && queryResult.results) ? queryResult.results : []
|
|
33
|
+
const sources = results.map((r) => ({
|
|
34
|
+
name: `${r.kind || 'memory'}: ${String(r.content || '').slice(0, 40)}`,
|
|
35
|
+
content: String(r.content || ''),
|
|
36
|
+
type: 'feedback',
|
|
37
|
+
weight: WEIGHTS.grounding,
|
|
38
|
+
origin: `memory:${r.id}`,
|
|
39
|
+
originId: r.id,
|
|
40
|
+
...(r.timestamp ? { timestamp: r.timestamp } : {}),
|
|
41
|
+
}))
|
|
42
|
+
return { sources, relatedMemoryIds: results.map((r) => r.id), groundedWith: results.length }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function namedSource(role, text, origin, weight) {
|
|
46
|
+
const body = String(text).trim()
|
|
47
|
+
return {
|
|
48
|
+
name: `${role}: ${body.slice(0, 40)}`,
|
|
49
|
+
content: body,
|
|
50
|
+
type: 'knowledge',
|
|
51
|
+
weight,
|
|
52
|
+
origin,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runIdeaGenerate(options) {
|
|
57
|
+
const {
|
|
58
|
+
goal,
|
|
59
|
+
context = '',
|
|
60
|
+
constraints = [],
|
|
61
|
+
numIdeas,
|
|
62
|
+
chatJson,
|
|
63
|
+
store,
|
|
64
|
+
groundingSources = [],
|
|
65
|
+
relatedMemoryIds = [],
|
|
66
|
+
autoSources = [],
|
|
67
|
+
} = options || {}
|
|
68
|
+
|
|
69
|
+
const ordered = []
|
|
70
|
+
if (typeof goal === 'string' && goal.trim()) ordered.push(namedSource('goal', goal, 'goal', WEIGHTS.goal))
|
|
71
|
+
if (typeof context === 'string' && context.trim()) ordered.push(namedSource('context', context, 'context', WEIGHTS.context))
|
|
72
|
+
if (Array.isArray(constraints)) {
|
|
73
|
+
constraints.forEach((c, i) => {
|
|
74
|
+
if (typeof c === 'string' && c.trim()) ordered.push(namedSource('constraint', c, `constraint:${i}`, WEIGHTS.constraint))
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
for (const s of [...groundingSources, ...autoSources]) {
|
|
78
|
+
if (s && typeof s === 'object' && s.name) ordered.push(s)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const seen = new Set()
|
|
82
|
+
const sources = []
|
|
83
|
+
for (const s of ordered) {
|
|
84
|
+
const key = s.origin || s.name
|
|
85
|
+
if (seen.has(key)) continue
|
|
86
|
+
seen.add(key)
|
|
87
|
+
sources.push(s)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sources.length < 2) {
|
|
91
|
+
return { reason: 'need at least 2 sources — add --context/--constraints, or run `mio remember` first' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pairingStrategy = new Set(sources.map((s) => s.type)).size >= 2 ? 'explore' : 'stable'
|
|
95
|
+
const requested = Number(numIdeas)
|
|
96
|
+
const maxIdeas = Number.isFinite(requested) ? Math.min(Math.max(Math.trunc(requested), 1), 3) : 3
|
|
97
|
+
|
|
98
|
+
const rotationSeed = store.getHypotheses().length
|
|
99
|
+
const generator = new IdeaGenerator(chatJson, 0.5, undefined, rotationSeed)
|
|
100
|
+
// method left undefined: HypothesisGenerator rotates techniques internally
|
|
101
|
+
// from rotationSeed; the chosen one comes back on hypothesis.technique.
|
|
102
|
+
const creativeIdeas = await generator.generateIdeas(sources, maxIdeas, pairingStrategy, [])
|
|
103
|
+
|
|
104
|
+
const generatedAt = new Date().toISOString()
|
|
105
|
+
const recent = store.getHypotheses().slice(-30)
|
|
106
|
+
const rejected = store.getHypotheses({ status: 'rejected' }).slice(-50)
|
|
107
|
+
|
|
108
|
+
const ideas = []
|
|
109
|
+
const persistedIds = []
|
|
110
|
+
for (const { hypothesis: h, experiment } of creativeIdeas) {
|
|
111
|
+
const verdict = evaluateNovelty(h, recent, rejected)
|
|
112
|
+
if (verdict.shouldReject) {
|
|
113
|
+
h.status = 'rejected'
|
|
114
|
+
h.rejectionReason = verdict.rejectReason
|
|
115
|
+
} else {
|
|
116
|
+
h.novelty = verdict.adjustedNovelty
|
|
117
|
+
}
|
|
118
|
+
const labels = new Set(Array.isArray(h.sourceLabels) ? h.sourceLabels : [])
|
|
119
|
+
h.provenance = {
|
|
120
|
+
strategy: pairingStrategy,
|
|
121
|
+
technique: h.technique,
|
|
122
|
+
relatedMemoryIds: [...relatedMemoryIds],
|
|
123
|
+
sources: sources
|
|
124
|
+
.filter((s) => labels.has(s.name))
|
|
125
|
+
.map((s) => ({ name: s.name, type: s.type, origin: s.origin, timestamp: s.timestamp })),
|
|
126
|
+
generatedAt,
|
|
127
|
+
}
|
|
128
|
+
store.addHypothesis(h)
|
|
129
|
+
persistedIds.push(h.id)
|
|
130
|
+
ideas.push({ title: h.title, hypothesis: h, experiment, novelty: h.novelty, provenance: h.provenance })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { ideas, generatedAt, groundedWith: groundingSources.length, persistedIds }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { runIdeaGenerate, groundingFrom }
|
|
@@ -21,6 +21,8 @@ const { createDigest } = require('../digest.js')
|
|
|
21
21
|
const { createEvolutionReport } = require('../evolution-report.js')
|
|
22
22
|
|
|
23
23
|
const { CreativityEngine, sourcesFromInsights } = require('../creativity-engine.js')
|
|
24
|
+
const { buildAutoSources, topUpSources } = require('../creativity-sources.js')
|
|
25
|
+
const { runIdeaGenerate, groundingFrom } = require('../idea-generate.js')
|
|
24
26
|
// The observer and insight engines are optional dependencies. Their stores own
|
|
25
27
|
// the require and expose isObserverAvailable() / isInsightAvailable() so tool
|
|
26
28
|
// registration can be gated on them, and both the MCP server and the CLI share
|
|
@@ -178,6 +180,16 @@ const memoryStore = createMemoryStore({
|
|
|
178
180
|
verifiedFilter: REUSE_STATUS_FILTERS.verified,
|
|
179
181
|
})
|
|
180
182
|
|
|
183
|
+
// Second memory-store instance WITHOUT the shared queryLog: idea.generate's
|
|
184
|
+
// grounding recall is tool-side context gathering, not a user query — it must
|
|
185
|
+
// not append to queries.jsonl (that log exists for recall-style queries).
|
|
186
|
+
const groundingMemoryStore = createMemoryStore({
|
|
187
|
+
dataDir,
|
|
188
|
+
projectName,
|
|
189
|
+
agentId: runtimeAgentId,
|
|
190
|
+
verifiedFilter: REUSE_STATUS_FILTERS.verified,
|
|
191
|
+
})
|
|
192
|
+
|
|
181
193
|
const digestEngine = createDigest({ home: dataDir })
|
|
182
194
|
|
|
183
195
|
const {
|
|
@@ -695,6 +707,7 @@ const TOOLS = [
|
|
|
695
707
|
properties: {
|
|
696
708
|
status: { type: 'string', description: 'Filter by status: active, validated, rejected, draft.' },
|
|
697
709
|
limit: { type: 'number', description: 'Max results. Default 20.' },
|
|
710
|
+
sort: { type: 'string', description: "Sort order: 'novelty' ranks stored hypotheses by relative noveltyScore (a dedup signal, NOT a quality score), newest 200 candidates capped. Default: newest." },
|
|
698
711
|
},
|
|
699
712
|
},
|
|
700
713
|
},
|
|
@@ -736,6 +749,21 @@ const TOOLS = [
|
|
|
736
749
|
},
|
|
737
750
|
},
|
|
738
751
|
},
|
|
752
|
+
{
|
|
753
|
+
name: 'mio.idea.generate',
|
|
754
|
+
description:
|
|
755
|
+
'Generate up to 3 concrete, experiment-backed hypotheses from one goal. Sources are assembled locally (goal, context/constraints, Mio memory grounding, local memory/traces/stored hypotheses) and every idea is persisted to the CreativityStore with provenance — Mio memory is never written. Returns { ideas, generatedAt, groundedWith, persistedIds }, or { reason } when fewer than two sources are available.',
|
|
756
|
+
inputSchema: {
|
|
757
|
+
type: 'object',
|
|
758
|
+
properties: {
|
|
759
|
+
goal: { type: 'string', description: 'The one-line goal the ideas should pursue (required).' },
|
|
760
|
+
context: { type: 'string', description: 'Optional free-form background.' },
|
|
761
|
+
constraints: { type: 'array', items: { type: 'string' }, description: 'Optional hard constraints.' },
|
|
762
|
+
numIdeas: { type: 'number', description: 'How many ideas to return; clamped to 1..3, default 3.' },
|
|
763
|
+
},
|
|
764
|
+
required: ['goal'],
|
|
765
|
+
},
|
|
766
|
+
},
|
|
739
767
|
|
|
740
768
|
// ═══ Observer Research Pipeline ═══
|
|
741
769
|
|
|
@@ -968,6 +996,25 @@ function observerDag(args = {}) {
|
|
|
968
996
|
return observerStore.dag(args)
|
|
969
997
|
}
|
|
970
998
|
|
|
999
|
+
// Local auto sources shared by generate's top-up and idea.generate — the same
|
|
1000
|
+
// providers the CLI assembles (spec Capability 2). Every layer degrades to []
|
|
1001
|
+
// so a missing optional package or an unreadable store never fails a tool call.
|
|
1002
|
+
function autoSources() {
|
|
1003
|
+
let insights = []
|
|
1004
|
+
let trends = []
|
|
1005
|
+
try {
|
|
1006
|
+
if (isInsightAvailable()) insights = insightStore.list({}) || []
|
|
1007
|
+
} catch (_) {}
|
|
1008
|
+
try {
|
|
1009
|
+
trends = observerStore.trends({ limit: 3 }) || []
|
|
1010
|
+
} catch (_) {}
|
|
1011
|
+
try {
|
|
1012
|
+
return buildAutoSources({ dataDir, insights, trends })
|
|
1013
|
+
} catch (_) {
|
|
1014
|
+
return []
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
971
1018
|
// `fromInsights` seeds generation from what the insight engine already noticed
|
|
972
1019
|
// instead of making the caller hand-write concept sources. Only this entry
|
|
973
1020
|
// point knows which store is authoritative, so the read happens here; the
|
|
@@ -975,15 +1022,24 @@ function observerDag(args = {}) {
|
|
|
975
1022
|
// so the CLI's `--from-insights` lands on the same shapes.
|
|
976
1023
|
function generationSources(args) {
|
|
977
1024
|
const supplied = args.sources
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1025
|
+
// Nothing to top up and no fromInsights: return as-is so the engine keeps
|
|
1026
|
+
// throwing its documented "requires sources" error for empty calls.
|
|
1027
|
+
if (!args.fromInsights && !(Array.isArray(supplied) && supplied.length)) return supplied
|
|
1028
|
+
let base = supplied
|
|
1029
|
+
if (args.fromInsights) {
|
|
1030
|
+
if (!isInsightAvailable()) {
|
|
1031
|
+
throw new Error('mio.creativity.generate fromInsights requires the optional package @akemi-mio/insight')
|
|
1032
|
+
}
|
|
1033
|
+
const seeds = sourcesFromInsights(insightStore.list({}))
|
|
1034
|
+
if (seeds.length === 0) {
|
|
1035
|
+
throw new Error('mio.creativity.generate fromInsights: no stored insights yet (run `mio insight generate` first)')
|
|
1036
|
+
}
|
|
1037
|
+
base = Array.isArray(supplied) ? supplied.concat(seeds) : seeds
|
|
985
1038
|
}
|
|
986
|
-
|
|
1039
|
+
// Explicit sources always win; local memory/traces/stored hypotheses only
|
|
1040
|
+
// top a short list up to the two entries the mixer needs (spec Capability 2,
|
|
1041
|
+
// injection point 2 — the MCP twin of `mio creativity generate`).
|
|
1042
|
+
return topUpSources(Array.isArray(base) ? base : [], autoSources())
|
|
987
1043
|
}
|
|
988
1044
|
|
|
989
1045
|
async function callTool(name, args = {}) {
|
|
@@ -1060,6 +1116,26 @@ async function callTool(name, args = {}) {
|
|
|
1060
1116
|
return creativityEngine.generate(generationSources(args), args.strategy)
|
|
1061
1117
|
case 'mio.creativity.ferment':
|
|
1062
1118
|
return creativityEngine.ferment(args.limit)
|
|
1119
|
+
case 'mio.idea.generate': {
|
|
1120
|
+
const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
|
|
1121
|
+
if (!goal) throw new Error('mio.idea.generate requires goal: a non-empty string')
|
|
1122
|
+
let grounding = { sources: [], relatedMemoryIds: [], groundedWith: 0 }
|
|
1123
|
+
try {
|
|
1124
|
+
const qr = await groundingMemoryStore.queryMemory({ query: goal, limit: 3, scope: 'all' })
|
|
1125
|
+
grounding = groundingFrom(qr)
|
|
1126
|
+
} catch (_) {}
|
|
1127
|
+
return runIdeaGenerate({
|
|
1128
|
+
goal,
|
|
1129
|
+
context: typeof args.context === 'string' ? args.context : '',
|
|
1130
|
+
constraints: Array.isArray(args.constraints) ? args.constraints.filter((c) => typeof c === 'string') : [],
|
|
1131
|
+
numIdeas: args.numIdeas,
|
|
1132
|
+
chatJson,
|
|
1133
|
+
store: creativityEngine.store,
|
|
1134
|
+
groundingSources: grounding.sources,
|
|
1135
|
+
relatedMemoryIds: grounding.relatedMemoryIds,
|
|
1136
|
+
autoSources: autoSources(),
|
|
1137
|
+
})
|
|
1138
|
+
}
|
|
1063
1139
|
case 'mio.insight.status':
|
|
1064
1140
|
return insightStatus()
|
|
1065
1141
|
case 'mio.insight.list':
|