mio-agent-runtime 0.6.0 → 0.7.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 +46 -7
- package/bin/mio.js +2861 -2562
- package/package.json +1 -1
- package/server/llm-client.js +102 -10
- package/server/mio-intelligence-mcp/index.js +3 -0
- package/server/observer-store.js +18 -9
package/package.json
CHANGED
package/server/llm-client.js
CHANGED
|
@@ -6,27 +6,111 @@
|
|
|
6
6
|
// mio-intelligence-mcp/index.js so both entry points call the same model with
|
|
7
7
|
// the same configuration, and so "is an LLM configured?" has one answer.
|
|
8
8
|
//
|
|
9
|
-
// Configuration
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// Configuration resolution order (highest first):
|
|
10
|
+
// 1. Environment variables
|
|
11
|
+
// LLM_API_URL OpenAI-compatible endpoint (default: opencode zen)
|
|
12
|
+
// LLM_KEY bearer token; empty means unauthenticated (common for
|
|
13
|
+
// a local Ollama / llama.cpp server)
|
|
14
|
+
// LLM_CHAT_MODEL model id, falls back to LLM_MODEL
|
|
15
|
+
// 2. config.json under MIO_HOME (written by `mio config llm`) — the `llm`
|
|
16
|
+
// object's apiUrl / apiKey / model keys
|
|
17
|
+
// 3. Built-in defaults
|
|
18
|
+
//
|
|
19
|
+
// Environment wins so a shell-level override can always beat a stored config
|
|
20
|
+
// without editing files. The config file is read lazily and cached, so a long
|
|
21
|
+
// running MCP process does not re-stat it on every call.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs')
|
|
24
|
+
const os = require('os')
|
|
25
|
+
const path = require('path')
|
|
14
26
|
|
|
15
27
|
const DEFAULT_API_URL = 'https://opencode.ai/zen/go/v1/chat/completions'
|
|
16
28
|
const DEFAULT_MODEL = 'deepseek-v4-flash'
|
|
17
29
|
|
|
30
|
+
// Resolve the directory holding config.json. The CLI and the MCP server use
|
|
31
|
+
// different data dirs by design (MIO_HOME vs MIO_DATA_DIR||cwd/.mio-intelligence),
|
|
32
|
+
// so we honour whichever is set and fall back to the CLI default. No third
|
|
33
|
+
// convention is introduced here on purpose.
|
|
34
|
+
function homeDir() {
|
|
35
|
+
return (
|
|
36
|
+
process.env.MIO_HOME ||
|
|
37
|
+
process.env.MIO_DATA_DIR ||
|
|
38
|
+
path.join(os.homedir(), '.mio-intelligence')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let cachedFileConfig
|
|
43
|
+
let cachedFor = null
|
|
44
|
+
|
|
45
|
+
// Read the `llm` block from config.json. Any failure (missing file, bad JSON,
|
|
46
|
+
// wrong shape) degrades to {} — configuration must never crash a caller.
|
|
47
|
+
function fileConfig() {
|
|
48
|
+
const target = homeDir()
|
|
49
|
+
if (cachedFor === target) return cachedFileConfig
|
|
50
|
+
cachedFor = target
|
|
51
|
+
cachedFileConfig = {}
|
|
52
|
+
try {
|
|
53
|
+
const raw = fs.readFileSync(path.join(target, 'config.json'), 'utf8')
|
|
54
|
+
const parsed = JSON.parse(raw)
|
|
55
|
+
const llm = parsed && typeof parsed.llm === 'object' && parsed.llm !== null ? parsed.llm : {}
|
|
56
|
+
const pick = (v) => (typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined)
|
|
57
|
+
cachedFileConfig = {
|
|
58
|
+
apiUrl: pick(llm.apiUrl),
|
|
59
|
+
apiKey: pick(llm.apiKey),
|
|
60
|
+
model: pick(llm.model),
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {
|
|
63
|
+
cachedFileConfig = {}
|
|
64
|
+
}
|
|
65
|
+
return cachedFileConfig
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Exposed so writers (`mio config llm`) can drop the cache after editing the
|
|
69
|
+
// file, and so tests can reset between cases.
|
|
70
|
+
function resetLlmConfigCache() {
|
|
71
|
+
cachedFor = null
|
|
72
|
+
cachedFileConfig = {}
|
|
73
|
+
}
|
|
74
|
+
|
|
18
75
|
function llmConfig() {
|
|
76
|
+
const file = fileConfig()
|
|
19
77
|
return {
|
|
20
|
-
apiUrl: process.env.LLM_API_URL || DEFAULT_API_URL,
|
|
21
|
-
apiKey: process.env.LLM_KEY || '',
|
|
22
|
-
model: process.env.LLM_CHAT_MODEL || process.env.LLM_MODEL || DEFAULT_MODEL,
|
|
78
|
+
apiUrl: process.env.LLM_API_URL || file.apiUrl || DEFAULT_API_URL,
|
|
79
|
+
apiKey: process.env.LLM_KEY || file.apiKey || '',
|
|
80
|
+
model: process.env.LLM_CHAT_MODEL || process.env.LLM_MODEL || file.model || DEFAULT_MODEL,
|
|
23
81
|
}
|
|
24
82
|
}
|
|
25
83
|
|
|
26
84
|
// True when the user has pointed us somewhere other than the default hosted
|
|
27
85
|
// endpoint, or supplied a key. Used to warn (not block) before spending a call.
|
|
86
|
+
// "Configured" counts either source: an env var or a stored config.json entry.
|
|
28
87
|
function isLlmConfigured() {
|
|
29
|
-
|
|
88
|
+
const file = fileConfig()
|
|
89
|
+
return Boolean(
|
|
90
|
+
process.env.LLM_API_URL ||
|
|
91
|
+
process.env.LLM_KEY ||
|
|
92
|
+
file.apiUrl ||
|
|
93
|
+
file.apiKey ||
|
|
94
|
+
file.model
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Which source each field came from, for `mio config show` to display honestly.
|
|
99
|
+
// Reporting a value without its origin is how users end up editing config.json
|
|
100
|
+
// and wondering why nothing changed (an env var was shadowing them).
|
|
101
|
+
function llmConfigSources() {
|
|
102
|
+
const file = fileConfig()
|
|
103
|
+
return {
|
|
104
|
+
apiUrl: process.env.LLM_API_URL ? 'env' : file.apiUrl ? 'config' : 'default',
|
|
105
|
+
apiKey: process.env.LLM_KEY ? 'env' : file.apiKey ? 'config' : 'unset',
|
|
106
|
+
model: process.env.LLM_CHAT_MODEL
|
|
107
|
+
? 'env'
|
|
108
|
+
: process.env.LLM_MODEL
|
|
109
|
+
? 'env'
|
|
110
|
+
: file.model
|
|
111
|
+
? 'config'
|
|
112
|
+
: 'default',
|
|
113
|
+
}
|
|
30
114
|
}
|
|
31
115
|
|
|
32
116
|
async function chatJson(userText, opts = {}) {
|
|
@@ -66,4 +150,12 @@ async function chatJson(userText, opts = {}) {
|
|
|
66
150
|
}
|
|
67
151
|
}
|
|
68
152
|
|
|
69
|
-
module.exports = {
|
|
153
|
+
module.exports = {
|
|
154
|
+
chatJson,
|
|
155
|
+
llmConfig,
|
|
156
|
+
isLlmConfigured,
|
|
157
|
+
llmConfigSources,
|
|
158
|
+
resetLlmConfigCache,
|
|
159
|
+
DEFAULT_API_URL,
|
|
160
|
+
DEFAULT_MODEL,
|
|
161
|
+
}
|
|
@@ -913,6 +913,9 @@ function observerStatus(args = {}) {
|
|
|
913
913
|
return observerStore.status(args)
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
+
// Both return promises (the upstream collector and fermentation engine are
|
|
917
|
+
// async). callTool is async, so the promise is awaited before the result is
|
|
918
|
+
// serialized -- but nothing here may treat them as synchronous values.
|
|
916
919
|
function observerCollect(args = {}) {
|
|
917
920
|
return observerStore.collect(args)
|
|
918
921
|
}
|
package/server/observer-store.js
CHANGED
|
@@ -161,19 +161,28 @@ function createObserverStore(options = {}) {
|
|
|
161
161
|
return { summaries, summaryCount: summaries.reduce((a, s) => a + s.summaries.length, 0) }
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// ObserverService.collectBySource and FermentationEngine.ferment are both
|
|
165
|
+
// async. They must be awaited here: a Promise is `typeof 'object'` with no
|
|
166
|
+
// own enumerable keys, so the previous synchronous version of collect()
|
|
167
|
+
// happily "iterated" one and pushed nothing -- every call reported
|
|
168
|
+
// `collected: 0` no matter how much the collectors actually returned.
|
|
169
|
+
async function collect(args = {}) {
|
|
165
170
|
if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
|
|
166
171
|
const service = new ObserverService({ baseDir: baseDirOf(args), collectorConfig: {} })
|
|
167
172
|
const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
|
|
168
173
|
const allObs = []
|
|
169
174
|
for (const src of sources) {
|
|
170
175
|
try {
|
|
171
|
-
const collectFn = service.collectBySource
|
|
172
|
-
if (collectFn) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
const collectFn = service.collectBySource || service.collect
|
|
177
|
+
if (!collectFn) {
|
|
178
|
+
throw new Error('ObserverService exposes neither collectBySource nor collect')
|
|
179
|
+
}
|
|
180
|
+
const result = await collectFn.call(service, [src], args.keywords || [], args.limit || 20)
|
|
181
|
+
if (Array.isArray(result)) allObs.push(...result)
|
|
182
|
+
else if (result && typeof result === 'object') {
|
|
183
|
+
// Record<sourceName, Observation[]>: keep every list, in key order.
|
|
184
|
+
for (const items of Object.values(result)) {
|
|
185
|
+
if (Array.isArray(items)) allObs.push(...items)
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
} catch (e) {
|
|
@@ -183,13 +192,13 @@ function createObserverStore(options = {}) {
|
|
|
183
192
|
return { collected: allObs.length, observations: allObs }
|
|
184
193
|
}
|
|
185
194
|
|
|
186
|
-
function ferment(args = {}) {
|
|
195
|
+
async function ferment(args = {}) {
|
|
187
196
|
if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
|
|
188
197
|
const service = new ObserverService({ baseDir: baseDirOf(args), collectorConfig: {} })
|
|
189
198
|
const session = args.session || 'afternoon'
|
|
190
199
|
try {
|
|
191
200
|
const fermentation = service.getFermentation()
|
|
192
|
-
if (fermentation && fermentation.ferment) return fermentation.ferment(session)
|
|
201
|
+
if (fermentation && fermentation.ferment) return await fermentation.ferment(session)
|
|
193
202
|
return { status: 'fermentation engine available but no ferment method' }
|
|
194
203
|
} catch (e) {
|
|
195
204
|
return { error: e.message }
|