mio-agent-runtime 0.12.0 → 0.13.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
@@ -65,6 +65,7 @@ mio observer <view> Observer research pipeline views (research pipeline
65
65
  status|world-model|trends|research|insights|essays|dag (--base-dir DIR)
66
66
  mio observer collect Fetch from the configured sources (--sources a,b/--keywords k1,k2/--limit N)
67
67
  mio observer ferment Run the fermentation engine (--session morning|afternoon|night)
68
+ mio observer pipeline Run the research DAG (previews by default; --run executes, --mode neutral|analytical|creative)
68
69
  mio observer ingest --trace-id T --event-type E Record a trace event (--payload JSON/--outcome)
69
70
  mio observer subscribe --event-types a,b Subscribe to events (--yes to apply; previews by default)
70
71
  mio observer digest New events since the last digest (advances the cursor)
@@ -99,13 +100,13 @@ mio --version Print the version and exit (-V)
99
100
 
100
101
  ## MCP 工具
101
102
 
102
- MCP 服务端在 5 大域共暴露 49 个工具:
103
+ MCP 服务端在 5 大域共暴露 50 个工具:
103
104
 
104
105
  ### 记忆(11)
105
106
  `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`
106
107
 
107
- ### 观察管线(14)
108
- `mio.observer.world_model` · `mio.observer.trends` · `mio.observer.research` · `mio.observer.insights` · `mio.observer.status` · `mio.observer.collect` · `mio.observer.ferment` · `mio.observer.essays` · `mio.observer.dag` · `mio.observer.ingest` · `mio.observer.subscribe` · `mio.observer.digest`
108
+ ### 观察管线(15)
109
+ `mio.observer.world_model` · `mio.observer.trends` · `mio.observer.research` · `mio.observer.insights` · `mio.observer.status` · `mio.observer.collect` · `mio.observer.ferment` · `mio.observer.pipeline` · `mio.observer.essays` · `mio.observer.dag` · `mio.observer.ingest` · `mio.observer.subscribe` · `mio.observer.digest`
109
110
  `mio.trace.query` — 按类型/结果/agent/项目/时间窗口查询 trace 日志(任务结果、工具错误);CLI 上也可通过 `mio traces` 调用
110
111
  `mio.digest.generate` — 把近期数据聚合成可执行的 digest(agent 成功率、项目活跃度、错误热点、复用证据、建议);CLI 上也可通过 `mio digest` 调用
111
112
 
@@ -490,6 +491,15 @@ mio observer trends --base-dir /path/to/.local/observer
490
491
  ```
491
492
 
492
493
  两者都依赖可选包 `@akemi-mio/observer`(未安装时提示 `not installed`,不是空结果)。`collect` 会**联网**抓取,`ferment` 需要 LLM;它们**不做预览**,因为预览意味着把数据抓两遍。单个源失败不会中断整次运行——该源会以 `errors: ...` 出现在输出里,其余源照常统计。
494
+ - **`mio observer pipeline` 是研究 DAG 的入口**(`collect → trend → tension → research → multi-brain → compose → world model → publish → self-evolve`)。此前 `runPipeline` / `tickPipeline` / `forcePipeline` 在 mio-agent-runtime 里**没有任何调用方**,所以 trends / research / insights 默认永远是空的,只有手动实例化服务才能跑通。它同样依赖 `@akemi-mio/observer`,会**联网并调用多次 LLM**,因此和 `subscribe` 一样**默认只预览**(读今天的 DAG 状态与解析后的 LLM 端点,不构造服务、不建目录),`--run` 才真正执行:
495
+
496
+ ```bash
497
+ mio observer pipeline # 预览(离线、秒回)
498
+ mio observer pipeline --run # 执行完整 DAG
499
+ mio observer pipeline --mode creative --run
500
+ ```
501
+
502
+ - **观察者的 LLM 现在与 creativity / insight 共用同一份配置**。此前 `ObserverLlmService` 把 Ollama 地址与模型写死(`localhost:11434` / `qwen2.5:7b`,只认 `OBSERVER_*` 环境变量),完全无视 `mio config llm` —— 于是 `mio config llm` 配好的 deepseek 只对 server 侧生效,观察管道仍然空转。现在 `observer-store.js` 通过共享的 `server/llm-client.js` 解析配置并注入:用户配置过(`config.json` 的 `llm` 或 `LLM_*` 环境变量)就用它,否则保持 `@akemi-mio/observer` 自己的 `LLM_*` / `OBSERVER_*` / 本地 Ollama 回退,不会把既有本地 Ollama 用户静默改道到托管默认值。端点按 URL 形态自动选择传输:含 `/chat/completions` 走 OpenAI 兼容协议,否则按 Ollama 处理。
493
503
  - **`mio observer ingest` 记录任意 trace 事件**(`tool_call` / `error` / `retry` / `task_outcome`):
494
504
 
495
505
  ```bash
package/bin/mio.js CHANGED
@@ -1530,6 +1530,9 @@ function observerUsage(write = console.log) {
1530
1530
  [--limit N] (needs @akemi-mio/observer; hits the network)
1531
1531
  mio observer ferment Run the fermentation engine over recent observations
1532
1532
  (--session morning|afternoon|night; needs @akemi-mio/observer)
1533
+ mio observer pipeline Run the full research DAG (collect → trend → research →
1534
+ insights → world model). Previews by default; --run
1535
+ executes it (network + LLM). --mode neutral|analytical|creative
1533
1536
 
1534
1537
  Options:
1535
1538
  --base-dir DIR Observer data directory (default: <cwd>/.local/observer)
@@ -1540,6 +1543,8 @@ Options:
1540
1543
  --sources a,b Sources to collect from (default: all configured)
1541
1544
  --keywords a,b Keyword filter, OR matched (collect)
1542
1545
  --session label Fermentation session (default: afternoon)
1546
+ --mode name Writing mode for the pipeline (default: analytical)
1547
+ --run Actually execute the pipeline (default: preview)
1543
1548
  --json Machine-readable output (same shape as mio.observer.*)`)
1544
1549
  }
1545
1550
 
@@ -1854,6 +1859,62 @@ async function observerFermentCommand(args, useJson) {
1854
1859
  printObserverFerment(result, session)
1855
1860
  }
1856
1861
 
1862
+ // The pipeline entry point D1 was missing: runPipeline/tickPipeline/forcePipeline
1863
+ // had no caller in mio-agent-runtime, so the observer's trend/research/insight
1864
+ // stages never ran on their own. It is heavy (network + several LLM calls), so
1865
+ // it follows the same rule as `mio observer subscribe` and `mio task
1866
+ // record-outcome`: preview by default, execute only with --run.
1867
+ const PIPELINE_MODES = ['neutral', 'analytical', 'creative']
1868
+
1869
+ function printObserverPipeline(result) {
1870
+ if (result.dryRun) {
1871
+ console.log(`Observer pipeline preview (mode=${result.mode}, base-dir=${result.baseDir})`)
1872
+ const state = result.todayState || 'no task yet'
1873
+ const done = result.todayCompleted ? ' (already completed)' : ''
1874
+ console.log(` today ${result.taskId}: ${state}${done}`)
1875
+ if (result.llm) {
1876
+ const key = result.llm.hasApiKey ? 'key set' : 'no key'
1877
+ console.log(` LLM: ${result.llm.provider} ${result.llm.model} (${key})`)
1878
+ console.log(` ${result.llm.apiUrl}`)
1879
+ } else {
1880
+ console.log(' LLM: unknown (observer package did not expose ObserverLlmService)')
1881
+ }
1882
+ console.log('\nPreview only. Re-run with --run to execute the full DAG (network + LLM calls).')
1883
+ return
1884
+ }
1885
+ if (!result.completed) {
1886
+ console.log(`Observer pipeline did not complete: ${result.reason || 'unknown reason'}`)
1887
+ process.exitCode = 1
1888
+ return
1889
+ }
1890
+ console.log(`Observer pipeline completed (${result.type}, ${result.durationMs || 0}ms, ${result.llmCalls || 0} LLM call(s))`)
1891
+ if (result.topic) console.log(` topic: ${result.topic}`)
1892
+ console.log(` sections: ${result.sections}`)
1893
+ if (result.insightId) console.log(` insight: ${result.insightId}`)
1894
+ }
1895
+
1896
+ async function observerPipelineCommand(args, useJson) {
1897
+ const flags = args.slice(2)
1898
+ const mode = optionValue(flags, '--mode') || 'analytical'
1899
+ if (!PIPELINE_MODES.includes(mode)) {
1900
+ console.error(`--mode must be one of: ${PIPELINE_MODES.join(', ')}`)
1901
+ process.exitCode = 1
1902
+ return
1903
+ }
1904
+ const base = { baseDir: optionValue(flags, '--base-dir') }
1905
+ const run = flagPresent(flags, '--run')
1906
+ let result
1907
+ try {
1908
+ result = await cliObserverStore().pipeline({ ...base, mode, run })
1909
+ } catch (error) {
1910
+ console.error(error.message || error)
1911
+ process.exitCode = 1
1912
+ return
1913
+ }
1914
+ if (useJson) return jsonOrText(result, true)
1915
+ printObserverPipeline(result)
1916
+ }
1917
+
1857
1918
  function observerCommand(args, useJson) {
1858
1919
  const sub = args[1]
1859
1920
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
@@ -1868,6 +1929,7 @@ function observerCommand(args, useJson) {
1868
1929
  // returned rather than called -- main() is async and awaits them.
1869
1930
  if (sub === 'collect') return observerCollectCommand(args, useJson)
1870
1931
  if (sub === 'ferment') return observerFermentCommand(args, useJson)
1932
+ if (sub === 'pipeline') return observerPipelineCommand(args, useJson)
1871
1933
 
1872
1934
  if (!['status', 'world-model', 'trends', 'research', 'insights', 'essays', 'dag'].includes(sub)) {
1873
1935
  console.error(`Unknown observer subcommand: ${sub}`)
@@ -3131,6 +3193,7 @@ Usage:
3131
3193
  mio observer digest New events since the last digest (advances cursor)
3132
3194
  mio observer collect Fetch from the configured sources (--sources/--keywords/--limit)
3133
3195
  mio observer ferment Run the fermentation engine (--session morning|afternoon|night)
3196
+ mio observer pipeline Run the research DAG (previews; --run to execute, --mode neutral|analytical|creative)
3134
3197
  mio phase0 report Show the Phase 0 validation report (--project X, --format markdown)
3135
3198
  mio host capabilities Show what each host supports and whether it is installed
3136
3199
  mio task route "<task>" Which verified experiences apply to this task (--project/--scope/--limit)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -33,7 +33,7 @@
33
33
  "@akemi-mio/evolution-strategy": "^0.1.0",
34
34
  "@akemi-mio/evolution-safety": "^0.1.0",
35
35
  "@akemi-mio/evolution-scheduler": "^0.1.0",
36
- "@akemi-mio/observer": "^0.1.0",
36
+ "@akemi-mio/observer": "^0.1.1",
37
37
  "@akemi-mio/insight": "^0.1.0"
38
38
  },
39
39
  "main": "index.js",
@@ -860,6 +860,18 @@ const TOOLS = [
860
860
  },
861
861
  },
862
862
  },
863
+ {
864
+ name: 'mio.observer.pipeline',
865
+ description: 'Run the Observer research DAG (collect -> trend -> tension -> research -> multi-brain -> compose -> world model -> publish). Heavy: network plus several LLM calls. Previews by default; pass run:true to execute.',
866
+ inputSchema: {
867
+ type: 'object',
868
+ properties: {
869
+ mode: { type: 'string', description: 'Writing mode: neutral | analytical | creative (default: analytical)' },
870
+ run: { type: 'boolean', description: 'Execute the pipeline. Default false returns a dry-run plan without network or LLM calls.' },
871
+ baseDir: { type: 'string', description: 'Observer data directory' },
872
+ },
873
+ },
874
+ },
863
875
  {
864
876
  name: 'mio.observer.essays',
865
877
  description: 'List published or draft essays from the Observer.',
@@ -941,6 +953,10 @@ function observerFerment(args = {}) {
941
953
  return observerStore.ferment(args)
942
954
  }
943
955
 
956
+ function observerPipeline(args = {}) {
957
+ return observerStore.pipeline(args)
958
+ }
959
+
944
960
  function observerEssays(args = {}) {
945
961
  return observerStore.essays(args)
946
962
  }
@@ -1045,6 +1061,8 @@ async function callTool(name, args = {}) {
1045
1061
  return observerCollect(args)
1046
1062
  case 'mio.observer.ferment':
1047
1063
  return observerFerment(args)
1064
+ case 'mio.observer.pipeline':
1065
+ return observerPipeline(args)
1048
1066
  case 'mio.observer.essays':
1049
1067
  return observerEssays(args)
1050
1068
  case 'mio.observer.dag':
@@ -14,6 +14,19 @@
14
14
  const fs = require('fs')
15
15
  const path = require('path')
16
16
 
17
+ // The observer pipeline is one of the LLM users, so it must read the same
18
+ // resolved model configuration as creativity / insight. `isLlmConfigured()`
19
+ // gates the override: when the user never configured an LLM we pass nothing and
20
+ // let @akemi-mio/observer keep its own LLM_* / OBSERVER_* / Ollama fallback, so
21
+ // an existing local-Ollama setup is not silently redirected to the hosted
22
+ // default. This is the D2 fix -- the observer used to hardcode Ollama and
23
+ // ignore `mio config llm` entirely.
24
+ const { llmConfig, isLlmConfigured } = require('./llm-client.js')
25
+
26
+ function observerLlmConfig() {
27
+ return isLlmConfigured() ? llmConfig() : undefined
28
+ }
29
+
17
30
  // Observer research pipeline (optional -- installed via @akemi-mio/observer).
18
31
  // Loaded lazily at module level so the CLI can report availability without the
19
32
  // MCP server having to pass its own handles down.
@@ -31,10 +44,12 @@ function loadObserver() {
31
44
 
32
45
  let ObserverStore = null
33
46
  let ObserverService = null
47
+ let ObserverLlmService = null
34
48
  try {
35
49
  const obs = loadObserver()
36
50
  ObserverStore = obs.ObserverStore
37
51
  ObserverService = obs.ObserverService
52
+ ObserverLlmService = obs.ObserverLlmService
38
53
  } catch {}
39
54
 
40
55
  // Subdirectories that make up the pipeline. `observerStatus` counts files in
@@ -62,6 +77,17 @@ function readJsonSafe(filePath) {
62
77
  }
63
78
  }
64
79
 
80
+ // Today's DAG task id, matching DagStateMachine's `dag_YYYYMMDD` (LOCAL date).
81
+ // Duplicated here rather than importing DagStateMachine because its constructor
82
+ // mkdir's the dag directory, and the dry run below must not touch disk.
83
+ function todayTaskId() {
84
+ const d = new Date()
85
+ const y = d.getFullYear()
86
+ const m = String(d.getMonth() + 1).padStart(2, '0')
87
+ const day = String(d.getDate()).padStart(2, '0')
88
+ return `dag_${y}${m}${day}`
89
+ }
90
+
65
91
  function readJsonlSafe(filePath) {
66
92
  if (!fs.existsSync(filePath)) return []
67
93
  try {
@@ -184,7 +210,7 @@ function createObserverStore(options = {}) {
184
210
  // object made path.resolve() throw before any collector ran, so collect()
185
211
  // was dead on arrival for every caller. (collectorConfig was never read by
186
212
  // the package; it was invented here.)
187
- const service = new ObserverService(baseDirOf(args))
213
+ const service = new ObserverService(baseDirOf(args), observerLlmConfig())
188
214
  const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
189
215
  const allObs = []
190
216
  for (const src of sources) {
@@ -210,7 +236,7 @@ function createObserverStore(options = {}) {
210
236
 
211
237
  async function ferment(args = {}) {
212
238
  if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
213
- const service = new ObserverService(baseDirOf(args))
239
+ const service = new ObserverService(baseDirOf(args), observerLlmConfig())
214
240
  const session = args.session || 'afternoon'
215
241
  try {
216
242
  const fermentation = service.getFermentation()
@@ -221,6 +247,81 @@ function createObserverStore(options = {}) {
221
247
  }
222
248
  }
223
249
 
250
+ // Drives the full research DAG (collect → trend → tension → research →
251
+ // multi-brain → compose → world model → publish → self-evolve). This is the
252
+ // entry point D1 found missing: runPipeline/tickPipeline/forcePipeline had no
253
+ // caller anywhere in mio-agent-runtime, so trends/research/insights stayed
254
+ // empty forever unless someone hand-instantiated the service.
255
+ //
256
+ // It is heavy (network + several LLM calls) and writes files, so like every
257
+ // other side-effecting command here it previews by default: without
258
+ // `run: true` it reads today's DAG state and the resolved LLM endpoint and
259
+ // returns a plan without constructing the service, which would mkdir the dag
260
+ // directory. That keeps the README doc gate and the MCP live gate (both call
261
+ // with empty arguments) offline and instant.
262
+ async function pipeline(args = {}) {
263
+ if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
264
+ const baseDir = baseDirOf(args)
265
+ const mode = args.mode || 'analytical'
266
+ const llmConfig = observerLlmConfig()
267
+
268
+ if (!args.run) {
269
+ const taskId = todayTaskId()
270
+ const dag = readJsonSafe(path.join(baseDir, 'dag', `${taskId}.json`))
271
+ // Guard the diagnostic: an older published @akemi-mio/observer lacks
272
+ // ObserverLlmService.getInfo(), and a preview must never throw because of
273
+ // a package-version skew (the mio-cli side ships ahead of the package).
274
+ let llm = null
275
+ if (ObserverLlmService) {
276
+ try {
277
+ const svc = new ObserverLlmService(llmConfig)
278
+ if (svc && typeof svc.getInfo === 'function') llm = svc.getInfo()
279
+ } catch (_) {
280
+ llm = null
281
+ }
282
+ }
283
+ return {
284
+ dryRun: true,
285
+ baseDir,
286
+ mode,
287
+ observerAvailable: true,
288
+ taskId,
289
+ todayState: dag ? dag.state : null,
290
+ todayCompleted: Boolean(dag && dag.state === 'COMPLETED'),
291
+ llm,
292
+ hint: 'Re-run with --run (CLI) or {"run": true} (MCP) to execute the full DAG.',
293
+ }
294
+ }
295
+
296
+ const service = new ObserverService(baseDir, llmConfig)
297
+ const envelope = await service.forcePipeline(mode)
298
+ if (!envelope) {
299
+ return {
300
+ dryRun: false,
301
+ baseDir,
302
+ mode,
303
+ ran: true,
304
+ completed: false,
305
+ reason: 'forcePipeline returned null (today already completed, or a run is in progress)',
306
+ }
307
+ }
308
+ const payload = envelope.payload || {}
309
+ return {
310
+ dryRun: false,
311
+ baseDir,
312
+ mode,
313
+ ran: true,
314
+ completed: envelope.dagState && envelope.dagState.state === 'COMPLETED',
315
+ type: envelope.type,
316
+ taskId: envelope.dagState ? envelope.dagState.taskId : null,
317
+ topic: payload.topic || null,
318
+ sections: Array.isArray(payload.sections) ? payload.sections.length : 0,
319
+ insightId: payload.id || null,
320
+ durationMs: envelope.metadata ? envelope.metadata.taskDurationMs : null,
321
+ llmCalls: envelope.metadata ? envelope.metadata.llmCalls : null,
322
+ }
323
+ }
324
+
224
325
  return {
225
326
  baseDir: defaultBaseDir,
226
327
  status,
@@ -232,6 +333,7 @@ function createObserverStore(options = {}) {
232
333
  dag,
233
334
  collect,
234
335
  ferment,
336
+ pipeline,
235
337
  }
236
338
  }
237
339