mio-agent-runtime 0.16.0 → 0.17.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 CHANGED
@@ -58,6 +58,7 @@ mio creativity list List creativity hypotheses (--status active|validat
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
60
  mio creativity adopt Record an adoption event for a stored hypothesis (writes traces only)
61
+ mio creativity feedback Show feedback runtime mode, snapshot metrics, and rollout decision (read-only)
61
62
  mio idea generate --goal "..." Grounded idea pipeline: auto sources + novelty gate, persisted (calls an LLM)
62
63
  mio insight status Insight counts: total, reported, unreported, high-value (needs @akemi-mio/insight)
63
64
  mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
@@ -384,6 +385,7 @@ Historically risky: ...
384
385
  mio creativity status
385
386
  mio creativity list --status rejected --limit 10
386
387
  mio creativity list --json
388
+ mio creativity feedback
387
389
  ```
388
390
 
389
391
  ```text
@@ -404,6 +406,7 @@ Recent top ideas:
404
406
  - **`status` 计入 `draft`。** `draft` 是发酵中的真实状态(`ferment` 会同时处理 `draft` 与 `active`),此前漏计导致 `status` 与 `list --status draft` 对不上。
405
407
  - **空存储是正常状态。** 引擎运行之前,`status` 报告全零计数,`list` 显示 "No hypotheses match." 两者都是有效输出,不是错误。
406
408
  - **缓存会在文件变化时失效。** 三个 JSONL 的读取按 `(size, mtime)` 校验,因此另一个进程写入后长驻的 MCP 服务端无需重启就能看到——与 `server/insight-store.js` 同一套守卫。
409
+ - **`feedback` 只读展示正向反馈闭环状态**(`mio.creativity.feedback` 同面):当前运行时模式(`off`/`shadow`/`canary`/`full`,默认 `off` = 逐字节原行为)、不可变快照指标(概念对数、归因覆盖率、重复加权率)、观测运行摘要(`feedback-runtime-runs.jsonl`,`shadow`/`canary`/`full` 模式下 generate 自动落盘),以及按预固定阈值算出的放量判定(`rollback`/`advance`/`hold`)。阈值走 `MIO_FEEDBACK_MIN_STAGE_RUNS`/`MIO_FEEDBACK_MIN_COVERAGE`/`MIO_FEEDBACK_MAX_NEGATIVE_RATE`,fail-closed 解析。反馈只影响生成,**永不进 `checkPromotionGate`/`checkSubmitGate`**。
407
410
 
408
411
  ### 采用证据链(adopt / status / list)
409
412
 
package/bin/mio.js CHANGED
@@ -2060,6 +2060,7 @@ function creativityUsage(write = console.log) {
2060
2060
  write(`Usage:
2061
2061
  mio creativity status Show hypothesis counts, recent top ideas, adoption join
2062
2062
  mio creativity list List hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
2063
+ mio creativity feedback Show feedback runtime mode, snapshot metrics, and rollout decision (read-only)
2063
2064
 
2064
2065
  mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
2065
2066
  mio creativity ferment Review/refine active hypotheses (calls an LLM)
@@ -2078,6 +2079,7 @@ Examples:
2078
2079
  mio creativity status
2079
2080
  mio creativity list --status rejected --limit 10
2080
2081
  mio creativity list --sort novelty
2082
+ mio creativity feedback
2081
2083
  mio creativity generate --source "auth|token rotation" --source "cache|write-through"
2082
2084
  mio creativity generate --from-insights --source "auth|token rotation"
2083
2085
 
@@ -2428,7 +2430,7 @@ function creativityCommand(args, useJson) {
2428
2430
  if (sub === 'ferment') return creativityFermentCommand(args, useJson)
2429
2431
  if (sub === 'adopt') return creativityAdoptCommand(args, useJson)
2430
2432
 
2431
- if (!['status', 'list'].includes(sub)) {
2433
+ if (!['status', 'list', 'feedback'].includes(sub)) {
2432
2434
  console.error(`Unknown creativity subcommand: ${sub}`)
2433
2435
  creativityUsage(console.error)
2434
2436
  process.exitCode = 1
@@ -2442,6 +2444,8 @@ function creativityCommand(args, useJson) {
2442
2444
  try {
2443
2445
  if (sub === 'status') {
2444
2446
  result = engine.status()
2447
+ } else if (sub === 'feedback') {
2448
+ result = engine.feedbackStatus()
2445
2449
  } else {
2446
2450
  const sort = optionValue(flags, '--sort')
2447
2451
  if (sort !== undefined && sort !== 'novelty') {
@@ -2463,9 +2467,38 @@ function creativityCommand(args, useJson) {
2463
2467
 
2464
2468
  if (useJson) return jsonOrText(result, true)
2465
2469
  if (sub === 'status') printCreativityStatus(result)
2470
+ else if (sub === 'feedback') printCreativityFeedback(result)
2466
2471
  else printCreativityList(result)
2467
2472
  }
2468
2473
 
2474
+ function printCreativityFeedback(status) {
2475
+ console.log('Feedback runtime (positive feedback loop):')
2476
+ console.log(` mode: ${status.mode}`)
2477
+ const snap = status.snapshot
2478
+ if (snap) {
2479
+ console.log(
2480
+ ` snapshot: v${snap.version} pairs=${snap.pairs} events=${snap.eventCount} coverage=${snap.attributionCoverage.toFixed(3)} dupRate=${snap.duplicateApplicationRate.toFixed(3)}`,
2481
+ )
2482
+ } else {
2483
+ console.log(' snapshot: unavailable')
2484
+ }
2485
+ const t = status.thresholds
2486
+ const r = status.rollout
2487
+ console.log(
2488
+ ` runs: shadow=${r.metrics.shadowRuns} canary=${r.metrics.canaryRuns} (min per stage ${t.minStageRuns})`,
2489
+ )
2490
+ console.log(` rollout: ${r.decision}${r.to ? ` → ${r.to}` : ''} (from ${r.from})`)
2491
+ for (const reason of r.reasons) console.log(` - ${reason}`)
2492
+ if (status.recentRuns.length > 0) {
2493
+ console.log(' recent runs:')
2494
+ for (const run of status.recentRuns) {
2495
+ console.log(
2496
+ ` ${new Date(run.at).toISOString().slice(0, 19)} ${run.mode} v${run.snapshotVersion || '-'} pairs=${run.pairCount} applied=${run.applied}`,
2497
+ )
2498
+ }
2499
+ }
2500
+ }
2501
+
2469
2502
  function mechanismUsage(write = console.log) {
2470
2503
  write(`mio mechanism <subcommand> — mechanism registry (promotion/rollback)
2471
2504
 
@@ -3582,6 +3615,7 @@ Usage:
3582
3615
  mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
3583
3616
  mio creativity status Show creativity hypothesis counts, recent top ideas, adoption join
3584
3617
  mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --sort novelty, --limit N)
3618
+ mio creativity feedback Show feedback runtime mode, snapshot metrics, and rollout decision (read-only)
3585
3619
  mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
3586
3620
  mio creativity ferment Review and refine active hypotheses (calls an LLM)
3587
3621
  mio creativity adopt Record an adoption event for a stored hypothesis (writes traces only)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -36,7 +36,7 @@
36
36
  "@akemi-mio/evolution-scheduler": "^0.1.0",
37
37
  "@akemi-mio/observer": "^0.1.5",
38
38
  "@akemi-mio/insight": "^0.1.1",
39
- "@akemi-mio/creativity": "^0.1.1"
39
+ "@akemi-mio/creativity": "^0.2.0"
40
40
  },
41
41
  "main": "index.js",
42
42
  "directories": {
@@ -5,6 +5,24 @@ const path = require('path')
5
5
  const crypto = require('crypto')
6
6
  const { readJsonl } = require('./memory-store.js')
7
7
  const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
8
+ const { JsonlFeedbackEventStore } = require('@akemi-mio/creativity/JsonlFeedbackEventStore')
9
+ const {
10
+ buildFeedbackSnapshot,
11
+ buildFeedbackPromptBlock,
12
+ replayFeedbackAdjustment,
13
+ DEFAULT_FEEDBACK_LEARNING_CONFIG,
14
+ } = require('@akemi-mio/creativity/FeedbackLearning')
15
+ const {
16
+ resolveFeedbackRuntime,
17
+ isFeedbackApplied,
18
+ isFeedbackObserved,
19
+ DEFAULT_FEEDBACK_RUNTIME,
20
+ FEEDBACK_MODES,
21
+ } = require('@akemi-mio/creativity/FeedbackRuntime')
22
+ const {
23
+ resolveFeedbackRolloutThresholds,
24
+ buildFeedbackRolloutReport,
25
+ } = require('@akemi-mio/creativity/FeedbackRollout')
8
26
 
9
27
  // Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
10
28
  // guard server/insight-store.js uses (commit 0ba7fa8). Every getter below used
@@ -172,7 +190,7 @@ function pairSourceKey(name) {
172
190
  return String(name == null ? '' : name).trim().slice(0, 120)
173
191
  }
174
192
 
175
- function mixConcepts(sources, exploredPairs, rng, strategy) {
193
+ function mixConcepts(sources, exploredPairs, rng, strategy, pairAdjustments) {
176
194
  if (sources.length < 2) return []
177
195
  // Dedup identity is (pair, strategy): `strategy` picks the prompt and the
178
196
  // temperature, so the documented fallback "hit 409, retry with another
@@ -199,6 +217,16 @@ function mixConcepts(sources, exploredPairs, rng, strategy) {
199
217
  const j = Math.floor(rng() * (i + 1))
200
218
  ;[pairs[i], pairs[j]] = [pairs[j], pairs[i]]
201
219
  }
220
+ // 正向反馈闭环 P3:按概念对反馈调整重排(缺省 pairAdjustments = 原顺序不变)。
221
+ // key 与快照 conceptPairKey 对齐(origin 优先,name 回退)。
222
+ if (pairAdjustments && pairAdjustments.size > 0) {
223
+ const fbId = (s) => (s.origin ? s.origin : `name:${s.name}`)
224
+ const adj = ([a, b]) => {
225
+ const k = [fbId(a), fbId(b)].sort().join('|')
226
+ return pairAdjustments.get(k) || 0
227
+ }
228
+ pairs.sort((x, y) => adj(y) - adj(x))
229
+ }
202
230
  return pairs.slice(0, Math.min(5, pairs.length))
203
231
  }
204
232
 
@@ -312,9 +340,109 @@ class CreativityEngine {
312
340
  this.strategyIndex = 0
313
341
  this.exploredPairs = new Set()
314
342
  this.rng = mulberry32(Date.now())
343
+ // 正向反馈闭环 P0:CLI 侧证据事件只追加 feedback-events.jsonl(交换格式),
344
+ // 权威真相源在主进程 SQLite,由 ingest 单向导入。
345
+ this.feedbackEvents = new JsonlFeedbackEventStore(dataDir)
346
+ // 正向反馈闭环 P3:受控启用运行时(默认 off,行为不变)。
347
+ this.feedbackRuntime = resolveFeedbackRuntime()
348
+ }
349
+
350
+ /** P3:热切换反馈模式(回滚 = 切回 off)。 */
351
+ setFeedbackRuntime(cfg) {
352
+ this.feedbackRuntime = cfg || DEFAULT_FEEDBACK_RUNTIME
353
+ return this.feedbackRuntime
354
+ }
355
+
356
+ /** P3:用本机 JSONL 事件 + 假设 provenance 构建不可变快照。 */
357
+ buildFeedbackSnapshot() {
358
+ const events = this.feedbackEvents.list()
359
+ const hypotheses = {}
360
+ for (const h of this.store.getHypotheses()) {
361
+ hypotheses[h.id] = h.provenance ? { sources: h.provenance.sources } : undefined
362
+ }
363
+ return buildFeedbackSnapshot(events, hypotheses, DEFAULT_FEEDBACK_LEARNING_CONFIG)
364
+ }
365
+
366
+ /** P3:把快照转成 mixer 调整表(key = 排序 conceptId 对)。 */
367
+ buildPairAdjustments(snapshot) {
368
+ const map = new Map()
369
+ for (const pair of snapshot.pairs) {
370
+ map.set(pair.pairKey, replayFeedbackAdjustment(pair, DEFAULT_FEEDBACK_LEARNING_CONFIG).clampedAdjustment)
371
+ }
372
+ return map
373
+ }
374
+
375
+ /** P3 收尾:观测运行摘要落 feedback-runtime-runs.jsonl(append-only,供放量报告)。 */
376
+ recordRuntimeRun(run) {
377
+ if (!fs.existsSync(this.dataDir)) fs.mkdirSync(this.dataDir, { recursive: true })
378
+ fs.appendFileSync(path.join(this.dataDir, 'feedback-runtime-runs.jsonl'), JSON.stringify(run) + '\n', 'utf8')
315
379
  }
316
380
 
317
- async generate(sourceLabels, strategy) {
381
+ readRuntimeRuns() {
382
+ const file = path.join(this.dataDir, 'feedback-runtime-runs.jsonl')
383
+ if (!fs.existsSync(file)) return []
384
+ const out = []
385
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
386
+ if (!line.trim()) continue
387
+ try {
388
+ const row = JSON.parse(line)
389
+ if (
390
+ row &&
391
+ typeof row === 'object' &&
392
+ typeof row.mode === 'string' &&
393
+ row.mode !== 'off' &&
394
+ FEEDBACK_MODES.includes(row.mode)
395
+ ) {
396
+ out.push({
397
+ at: Number(row.at) || 0,
398
+ mode: row.mode,
399
+ snapshotVersion: typeof row.snapshotVersion === 'string' ? row.snapshotVersion : null,
400
+ pairCount: Number(row.pairCount) || 0,
401
+ applied: !!row.applied,
402
+ })
403
+ }
404
+ } catch (_) {
405
+ // 损坏行跳过(同 JsonlFeedbackEventStore 策略)
406
+ }
407
+ }
408
+ return out
409
+ }
410
+
411
+ /** P3 收尾:放量状态 + 判定报告(只读;阈值走 env 配置,缺省预固定默认)。 */
412
+ feedbackStatus() {
413
+ let snapshot = null
414
+ try {
415
+ snapshot = this.buildFeedbackSnapshot()
416
+ } catch (_) {
417
+ snapshot = null
418
+ }
419
+ const runs = this.readRuntimeRuns()
420
+ const thresholds = resolveFeedbackRolloutThresholds()
421
+ const rollout = buildFeedbackRolloutReport({
422
+ snapshot,
423
+ runs,
424
+ currentMode: this.feedbackRuntime.mode,
425
+ thresholds,
426
+ })
427
+ return {
428
+ mode: this.feedbackRuntime.mode,
429
+ config: this.feedbackRuntime,
430
+ snapshot: snapshot
431
+ ? {
432
+ version: snapshot.version,
433
+ pairs: snapshot.pairs.length,
434
+ eventCount: snapshot.eventCount,
435
+ attributionCoverage: snapshot.attributionCoverage,
436
+ duplicateApplicationRate: snapshot.duplicateApplicationRate,
437
+ }
438
+ : null,
439
+ recentRuns: runs.slice(-5),
440
+ thresholds,
441
+ rollout,
442
+ }
443
+ }
444
+
445
+ async generate(sourceLabels, strategy, feedbackBlockArg) {
318
446
  // A missing sources array used to reach .map() and surface as
319
447
  // "Cannot read properties of undefined", which tells the caller nothing.
320
448
  if (!Array.isArray(sourceLabels)) {
@@ -325,6 +453,8 @@ class CreativityEngine {
325
453
  content: s.content || '',
326
454
  type: s.type || 'knowledge',
327
455
  weight: s.weight || 0.5,
456
+ // 正向反馈闭环 P2:保留 origin 供快照 pair 对齐(缺省 = 不影响既有行为)
457
+ ...(s.origin ? { origin: s.origin } : {}),
328
458
  }))
329
459
 
330
460
  if (sources.length < 2) {
@@ -349,7 +479,53 @@ class CreativityEngine {
349
479
  })
350
480
  )
351
481
  )
352
- const pairs = mixConcepts(sources, exploredPairs, this.rng.bind(this), strat)
482
+ // 正向反馈闭环 P3:受控启用。
483
+ // off = 原行为(缺省,逐字节不变)
484
+ // shadow = 只观测:计算快照与调整,记录版本,不应用到排序/prompt
485
+ // canary = 按比例应用(canaryFraction 缩放 λ)
486
+ // full = 全部应用
487
+ let feedbackBlock = feedbackBlockArg
488
+ let pairAdjustments = null
489
+ const feedbackInfo = { mode: this.feedbackRuntime.mode, version: null, pairs: 0, applied: false }
490
+ if (isFeedbackObserved(this.feedbackRuntime.mode) && !feedbackBlock) {
491
+ try {
492
+ const snapshot = this.buildFeedbackSnapshot()
493
+ feedbackInfo.version = snapshot.version
494
+ feedbackInfo.pairs = snapshot.pairs.length
495
+ if (snapshot.pairs.length > 0) {
496
+ const block = buildFeedbackPromptBlock(snapshot)
497
+ const adjustments = this.buildPairAdjustments(snapshot)
498
+ if (this.feedbackRuntime.mode === 'canary') {
499
+ const scale = this.feedbackRuntime.canaryFraction
500
+ for (const [k, v] of adjustments) adjustments.set(k, v * scale)
501
+ }
502
+ if (isFeedbackApplied(this.feedbackRuntime.mode)) {
503
+ feedbackBlock = block
504
+ pairAdjustments = adjustments
505
+ feedbackInfo.applied = true
506
+ }
507
+ // shadow:保持 block=null / adjustments=null,仅观测
508
+ }
509
+ } catch (_) {
510
+ // best-effort:反馈构建失败不得阻断生成
511
+ }
512
+ }
513
+ // P3 收尾:观测模式下落运行摘要,供放量报告统计阶段门槛。
514
+ if (isFeedbackObserved(this.feedbackRuntime.mode)) {
515
+ try {
516
+ this.recordRuntimeRun({
517
+ at: Date.now(),
518
+ mode: this.feedbackRuntime.mode,
519
+ snapshotVersion: feedbackInfo.version,
520
+ pairCount: feedbackInfo.pairs,
521
+ applied: feedbackInfo.applied,
522
+ })
523
+ } catch (_) {
524
+ // best-effort:观测落盘失败不得阻断生成
525
+ }
526
+ }
527
+
528
+ const pairs = mixConcepts(sources, exploredPairs, this.rng.bind(this), strat, pairAdjustments)
353
529
 
354
530
  if (pairs.length === 0) {
355
531
  // The bare string made a deterministic failure look like "this strategy
@@ -362,6 +538,7 @@ class CreativityEngine {
362
538
  reason: `all pairs already explored for strategy "${strat}" (${sources.length} sources, ${candidatePairs} candidate pairs)`,
363
539
  strategy: strat,
364
540
  pairsAvailable: 0,
541
+ feedback: feedbackInfo,
365
542
  }
366
543
  }
367
544
 
@@ -373,7 +550,7 @@ class CreativityEngine {
373
550
  // down, the JSON was unparseable, or every pair really was a duplicate.
374
551
  const errors = []
375
552
  for (const [a, b] of pairs) {
376
- const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5))
553
+ const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5), feedbackBlock)
377
554
  try {
378
555
  const result = await this.chatJson(prompt, {
379
556
  system: [
@@ -444,7 +621,7 @@ class CreativityEngine {
444
621
  }
445
622
  }
446
623
 
447
- return { ideas, strategy: strat, pairsAttempted: pairs.length, errors }
624
+ return { ideas, strategy: strat, pairsAttempted: pairs.length, errors, feedback: feedbackInfo }
448
625
  }
449
626
 
450
627
  async ferment(limit = 5) {
@@ -549,6 +726,23 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
549
726
  }
550
727
 
551
728
  this.store.updateHypothesis(h.id, patch)
729
+
730
+ // 正向反馈闭环 P0:ferment 终局追加证据事件(validated/rejected)。
731
+ // sourceId 按 ferment 轮次确定 → 同轮重放幂等;keep/merge 不产事件。
732
+ // 只写 JSONL 交换层,不改假设状态、不进晋级门。
733
+ if (patch.status === 'validated' || patch.status === 'rejected') {
734
+ const feedback = this.feedbackEvents.append({
735
+ hypothesisId: h.id,
736
+ eventType: patch.status,
737
+ sourceType: 'experiment',
738
+ sourceId: `ferment:${h.id}:${patch.fermentCount}`,
739
+ evidenceRef: typeof result.data.reason === 'string' ? result.data.reason.slice(0, 200) : undefined,
740
+ })
741
+ if (!feedback.appended && !feedback.deduped && feedback.quarantine) {
742
+ errors.push({ id: h.id, title: h.title, error: `feedback quarantine: ${feedback.quarantine.reason}` })
743
+ }
744
+ }
745
+
552
746
  results.push({ id: h.id, title: h.title, verdict, reason: result.data.reason })
553
747
  } catch (err) {
554
748
  errors.push({ id: h.id, title: h.title, error: String((err && err.message) || err) })
@@ -644,7 +838,7 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
644
838
  .map(decorate)
645
839
  }
646
840
 
647
- _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
841
+ _buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses, feedbackBlock) {
648
842
  // Long sources used to be pasted unbounded, crowding the instructions out
649
843
  // of the context window; 600 chars keeps the pairing signal without the
650
844
  // flood.
@@ -685,6 +879,12 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibili
685
879
  }
686
880
  }
687
881
 
882
+ // 正向反馈闭环 P2:结构化快照块(已验证来源 + 失败边界)。缺省 = 提示词不变。
883
+ if (feedbackBlock) {
884
+ const { buildFeedbackBlockSection } = require('@akemi-mio/creativity/CreativityPrompt')
885
+ prompt += `\n${buildFeedbackBlockSection(feedbackBlock)}`
886
+ }
887
+
688
888
  prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100, "logic": 0-100}`
689
889
  return prompt
690
890
  }
@@ -737,8 +937,9 @@ function adoptHypothesis(engine, args = {}, ingest, dataRoot) {
737
937
  }
738
938
  const taskId = String(args.taskId || '').trim()
739
939
  const note = String(args.note || '').trim()
940
+ const occurredAt = Date.now()
740
941
  const result = ingest({
741
- trace_id: `creativity-adopt:${hypothesisId}:${Date.now()}`,
942
+ trace_id: `creativity-adopt:${hypothesisId}:${occurredAt}`,
742
943
  event_type: 'creativity.adopt',
743
944
  outcome: 'success',
744
945
  payload: {
@@ -748,10 +949,22 @@ function adoptHypothesis(engine, args = {}, ingest, dataRoot) {
748
949
  ...(note ? { note } : {}),
749
950
  },
750
951
  })
952
+ // 正向反馈闭环 P0:adopt 是选择证据(weight 0.2 档),追加 adopted 事件到
953
+ // feedback-events.jsonl 交换层。store 状态不变(D1),不写 Memory(D4)。
954
+ // 每次有意的 adopt 调用各记一条事件;P1 聚合时在假设级归并,不虚增样本。
955
+ const feedback = engine.feedbackEvents.append({
956
+ hypothesisId,
957
+ eventType: 'adopted',
958
+ sourceType: 'adoption',
959
+ sourceId: `adopt:${hypothesisId}:${occurredAt}`,
960
+ ...(memoryId ? { evidenceRef: `memory:${memoryId}` } : {}),
961
+ occurredAt,
962
+ })
751
963
  return {
752
964
  recorded: true,
753
965
  hypothesisId,
754
966
  event: (result && result.event) || result,
967
+ feedback: { appended: feedback.appended, deduped: feedback.deduped },
755
968
  }
756
969
  }
757
970
 
@@ -803,6 +803,12 @@ const TOOLS = [
803
803
  required: ['hypothesisId'],
804
804
  },
805
805
  },
806
+ {
807
+ name: 'mio.creativity.feedback',
808
+ description:
809
+ 'Read-only positive-feedback-loop status: runtime mode (off/shadow/canary/full), immutable snapshot metrics (pairs, attribution coverage, duplicate application rate), recent observed runs, and the pre-fixed rollout decision (rollback/advance/hold with reasons). Rollout thresholds come from env (MIO_FEEDBACK_MIN_STAGE_RUNS / MIO_FEEDBACK_MIN_COVERAGE / MIO_FEEDBACK_MAX_NEGATIVE_RATE). Feedback never gates promotion or submit.',
810
+ inputSchema: { type: 'object', properties: {} },
811
+ },
806
812
  {
807
813
  name: 'mio.mechanism.registry',
808
814
  description:
@@ -1234,6 +1240,8 @@ async function callTool(name, args = {}) {
1234
1240
  return creativityEngine.ferment(args.limit)
1235
1241
  case 'mio.creativity.adopt':
1236
1242
  return adoptHypothesis(creativityEngine, args, ingestObservation, dataDir)
1243
+ case 'mio.creativity.feedback':
1244
+ return creativityEngine.feedbackStatus()
1237
1245
  case 'mio.mechanism.registry':
1238
1246
  return mechanismRegistry(args, dataDir)
1239
1247
  case 'mio.mechanism.promote':
@@ -54,15 +54,7 @@ try {
54
54
 
55
55
  // Subdirectories that make up the pipeline. `observerStatus` counts files in
56
56
  // each, so adding a stage here also makes it show up in the status output.
57
- const OBSERVER_SUBDIRS = [
58
- 'observations',
59
- 'trends',
60
- 'topics',
61
- 'research',
62
- 'insights',
63
- 'world_model',
64
- 'essays',
65
- ]
57
+ const OBSERVER_SUBDIRS = ['observations', 'trends', 'topics', 'research', 'insights', 'world_model', 'essays']
66
58
 
67
59
  function isObserverAvailable() {
68
60
  return Boolean(ObserverStore)
@@ -91,11 +83,7 @@ function todayTaskId() {
91
83
  function readJsonlSafe(filePath) {
92
84
  if (!fs.existsSync(filePath)) return []
93
85
  try {
94
- const lines = fs
95
- .readFileSync(filePath, 'utf8')
96
- .trim()
97
- .split('\n')
98
- .filter(Boolean)
86
+ const lines = fs.readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean)
99
87
  const results = []
100
88
  for (let i = 0; i < lines.length; i++) {
101
89
  try {
@@ -113,8 +101,15 @@ function readJsonlSafe(filePath) {
113
101
  // Reads a directory of per-item JSON files, newest file name first.
114
102
  function readJsonDir(dirPath, limit) {
115
103
  if (!fs.existsSync(dirPath)) return []
116
- const files = fs.readdirSync(dirPath).filter((f) => f.endsWith('.json')).sort().reverse()
117
- return files.slice(0, limit).map((f) => readJsonSafe(path.join(dirPath, f))).filter(Boolean)
104
+ const files = fs
105
+ .readdirSync(dirPath)
106
+ .filter((f) => f.endsWith('.json'))
107
+ .sort()
108
+ .reverse()
109
+ return files
110
+ .slice(0, limit)
111
+ .map((f) => readJsonSafe(path.join(dirPath, f)))
112
+ .filter(Boolean)
118
113
  }
119
114
 
120
115
  function createObserverStore(options = {}) {
@@ -131,9 +126,7 @@ function createObserverStore(options = {}) {
131
126
  const result = {}
132
127
  for (const d of OBSERVER_SUBDIRS) {
133
128
  const dir = path.join(baseDir, d)
134
- result[d] = fs.existsSync(dir)
135
- ? fs.readdirSync(dir).filter((f) => !f.startsWith('.')).length
136
- : 0
129
+ result[d] = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => !f.startsWith('.')).length : 0
137
130
  }
138
131
  return result
139
132
  }
@@ -172,7 +165,11 @@ function createObserverStore(options = {}) {
172
165
  const type = args.type || 'published'
173
166
  const essaysDir = path.join(baseDir, 'essays', type)
174
167
  if (!fs.existsSync(essaysDir)) return []
175
- const files = fs.readdirSync(essaysDir).filter((f) => f.endsWith('.md')).sort().reverse()
168
+ const files = fs
169
+ .readdirSync(essaysDir)
170
+ .filter((f) => f.endsWith('.md'))
171
+ .sort()
172
+ .reverse()
176
173
  return files.slice(0, args.limit || 10).map((f) => {
177
174
  const full = path.join(essaysDir, f)
178
175
  return {
@@ -212,7 +209,7 @@ function createObserverStore(options = {}) {
212
209
  // was dead on arrival for every caller. (collectorConfig was never read by
213
210
  // the package; it was invented here.)
214
211
  const service = new ObserverService(baseDirOf(args), observerLlmConfig())
215
- const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
212
+ const sources = args.sources || ['hackernews', 'github-trending', 'rss']
216
213
  const allObs = []
217
214
  for (const src of sources) {
218
215
  try {
@@ -354,7 +351,7 @@ function createObserverStore(options = {}) {
354
351
  const baseDir = baseDirOf(args)
355
352
  const llmConfig = observerLlmConfig()
356
353
  const schedule = {
357
- collectors: 'every source collects on start, then on its own interval (30m weibo / 1h rss,hn,douyin,bilibili / 4h github-trending)',
354
+ collectors: 'every source collects on start, then on its own interval (1h rss,hackernews / 4h github-trending)',
358
355
  pipelineTickMs: 60000,
359
356
  pipelineFirstTickMs: 5000,
360
357
  pipelineGate: "runs while today's DAG is not COMPLETED -- one full run per day, retried until it completes",