mio-agent-runtime 0.2.6 → 0.3.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 +61 -44
- package/adapters/workbuddy.js +7 -6
- package/bin/mio.js +84 -24
- package/observe/observer.js +452 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
# mio-agent-runtime
|
|
2
|
-
|
|
3
|
-
Mio Agent Runtime 的可发布 CLI 包。
|
|
4
|
-
|
|
5
|
-
安装一次,所有 Agent 自动接入 Mio。
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g mio-agent-runtime
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Commands
|
|
14
|
-
|
|
15
|
-
```text
|
|
16
|
-
mio init Initialize MIO_HOME
|
|
17
|
-
mio mcp Start Mio MCP server (stdio)
|
|
18
|
-
mio install <host> Install Mio into a host (codex|opencode|workbuddy)
|
|
19
|
-
mio status Show runtime and adapter status
|
|
20
|
-
mio agents List installed agents
|
|
21
|
-
mio
|
|
22
|
-
mio --
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
# mio-agent-runtime
|
|
2
|
+
|
|
3
|
+
Mio Agent Runtime 的可发布 CLI 包。
|
|
4
|
+
|
|
5
|
+
安装一次,所有 Agent 自动接入 Mio。
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g mio-agent-runtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
mio init Initialize MIO_HOME
|
|
17
|
+
mio mcp Start Mio MCP server (stdio)
|
|
18
|
+
mio install <host> Install Mio into a host (codex|opencode|workbuddy)
|
|
19
|
+
mio status Show runtime and adapter status
|
|
20
|
+
mio agents List installed agents
|
|
21
|
+
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes (foreground)
|
|
22
|
+
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
23
|
+
mio --json status Machine-readable status
|
|
24
|
+
mio --json agents Machine-readable agents
|
|
25
|
+
```
|
|
26
|
+
|
|
25
27
|
## Host notes
|
|
26
28
|
|
|
27
29
|
- OpenCode uses `~/.config/opencode/opencode.json` for MCP registration and
|
|
@@ -32,25 +34,40 @@ mio --json agents Machine-readable agents
|
|
|
32
34
|
- WorkBuddy uses `~/.workbuddy/mcp.json` and requires each custom MCP server to
|
|
33
35
|
be approved in `~/.workbuddy/mcp-approvals.json`. `mio install workbuddy`
|
|
34
36
|
writes both files and injects Mio usage rules into
|
|
35
|
-
`~/.workbuddy/
|
|
36
|
-
WorkBuddy into every session) plus `<workspace>/AGENTS.md` (project-level).
|
|
37
|
+
`~/.workbuddy/CODEBUDDY.md` (user-level memory rules, auto-loaded by
|
|
38
|
+
WorkBuddy into every session; the CLI hardcodes the CodeBuddy product name, so the file is CODEBUDDY.md not WORKBUDDY.md) plus `<workspace>/AGENTS.md` (project-level).
|
|
37
39
|
If WorkBuddy is already running, restart it once so it reloads the approval
|
|
38
|
-
and memory rule files.
|
|
39
|
-
|
|
40
|
-
##
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
and memory rule files.
|
|
41
|
+
|
|
42
|
+
## Passive observer (`mio observe`)
|
|
43
|
+
|
|
44
|
+
WorkBuddy 的默认模型不一定会主动调用 Mio MCP 工具,即使规则已注入
|
|
45
|
+
(`CODEBUDDY.md` / `AGENTS.md`)。`mio observe` 是一个不依赖模型自觉的
|
|
46
|
+
兜底:它持续监听 `~/.workbuddy/projects/**/*.jsonl` 会话记录,把每个任务的
|
|
47
|
+
结果(`task_outcome`)、工具调用错误(`error`)以及完成任务的摘要自动写入
|
|
48
|
+
`MIO_HOME/traces.jsonl` 和 `MIO_HOME/memory.jsonl`(与 MCP 服务同一套
|
|
49
|
+
schema,`source=workbuddy-observer`)。
|
|
50
|
+
|
|
51
|
+
- `mio install <host>` 会自动执行一次回填并启动后台观察器。
|
|
52
|
+
- 已处理的文件偏移记录在 `MIO_HOME/observe-state.json`,不会重复写入。
|
|
53
|
+
- 只回填最近 24h 内更新的会话,避免把历史记录灌入记忆库。
|
|
54
|
+
|
|
55
|
+
## MIO_HOME
|
|
56
|
+
|
|
57
|
+
默认 `~/.mio-intelligence`,可通过环境变量 `MIO_HOME` 覆盖。
|
|
58
|
+
|
|
59
|
+
## Package layout
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
packages/mio-cli
|
|
63
|
+
├── bin/mio.js
|
|
64
|
+
├── adapters/
|
|
65
|
+
│ ├── codex.js
|
|
66
|
+
│ ├── opencode.js
|
|
67
|
+
│ └── workbuddy.js
|
|
68
|
+
├── observe/
|
|
69
|
+
│ └── observer.js
|
|
70
|
+
├── server/
|
|
71
|
+
│ └── mio-intelligence-mcp/index.js
|
|
72
|
+
└── package.json
|
|
56
73
|
```
|
package/adapters/workbuddy.js
CHANGED
|
@@ -25,15 +25,16 @@ function approvalsPath() {
|
|
|
25
25
|
return path.join(configDir(), 'mcp-approvals.json')
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// User-level memory rules file. WorkBuddy's codebuddy CLI
|
|
29
|
-
//
|
|
30
|
-
// (MemoryLoader user scope -> system-reminder-md / prompt context).
|
|
28
|
+
// User-level memory rules file. WorkBuddy's codebuddy CLI (a CodeBuddy fork)
|
|
29
|
+
// auto-loads CODEBUDDY.md (or CODEBUDDY.mdc) from the config dir into every
|
|
30
|
+
// session (MemoryLoader user scope -> system-reminder-md / prompt context).
|
|
31
|
+
// Note: PRODUCT_UPPERCASE_NAME in the CLI bundle is "CODEBUDDY", not "WORKBUDDY".
|
|
31
32
|
function memoryRulesPath() {
|
|
32
|
-
return path.join(configDir(), '
|
|
33
|
+
return path.join(configDir(), 'CODEBUDDY.md')
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Project-level memory rules. WorkBuddy walks up from the workspace dir and
|
|
36
|
-
// auto-loads
|
|
37
|
+
// auto-loads CODEBUDDY.md / AGENTS.md (project scope) into every session.
|
|
37
38
|
function projectInstructionsPath(workspace) {
|
|
38
39
|
return path.join(workspace || process.cwd(), 'AGENTS.md')
|
|
39
40
|
}
|
|
@@ -188,7 +189,7 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
188
189
|
userRulesChanged: userRules.changed,
|
|
189
190
|
projectRulesChanged: projectRules ? projectRules.changed : false,
|
|
190
191
|
backup,
|
|
191
|
-
message: 'WorkBuddy Mio MCP installed, approved, and Mio rules injected into
|
|
192
|
+
message: 'WorkBuddy Mio MCP installed, approved, and Mio rules injected into CODEBUDDY.md (auto-loaded user memory) and the workspace AGENTS.md. Restart WorkBuddy to load them.',
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
package/bin/mio.js
CHANGED
|
@@ -14,6 +14,7 @@ const adapters = {
|
|
|
14
14
|
opencode: require('../adapters/opencode.js'),
|
|
15
15
|
workbuddy: require('../adapters/workbuddy.js'),
|
|
16
16
|
}
|
|
17
|
+
const observer = require('../observe/observer.js')
|
|
17
18
|
|
|
18
19
|
function ensureHome() {
|
|
19
20
|
fs.mkdirSync(MIO_HOME, { recursive: true })
|
|
@@ -51,33 +52,36 @@ function initCommand() {
|
|
|
51
52
|
console.log(`MIO_HOME ready: ${MIO_HOME}`)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function statusCommand(useJson) {
|
|
55
|
-
const config = readConfig()
|
|
56
|
-
const installed = {
|
|
57
|
-
codex: adapters.codex.isInstalled(),
|
|
58
|
-
opencode: adapters.opencode.isInstalled(),
|
|
59
|
-
workbuddy: adapters.workbuddy.isInstalled(),
|
|
60
|
-
}
|
|
61
|
-
const payload = {
|
|
62
|
-
version: config.version,
|
|
63
|
-
home: MIO_HOME,
|
|
64
|
-
serverScript: SERVER_SCRIPT,
|
|
65
|
-
serverScriptExists: fs.existsSync(SERVER_SCRIPT),
|
|
66
|
-
agents: config.agents || {},
|
|
67
|
-
codexInstalled: installed.codex,
|
|
68
|
-
opencodeInstalled: installed.opencode,
|
|
69
|
-
workbuddyInstalled: installed.workbuddy,
|
|
70
|
-
|
|
55
|
+
function statusCommand(useJson) {
|
|
56
|
+
const config = readConfig()
|
|
57
|
+
const installed = {
|
|
58
|
+
codex: adapters.codex.isInstalled(),
|
|
59
|
+
opencode: adapters.opencode.isInstalled(),
|
|
60
|
+
workbuddy: adapters.workbuddy.isInstalled(),
|
|
61
|
+
}
|
|
62
|
+
const payload = {
|
|
63
|
+
version: config.version,
|
|
64
|
+
home: MIO_HOME,
|
|
65
|
+
serverScript: SERVER_SCRIPT,
|
|
66
|
+
serverScriptExists: fs.existsSync(SERVER_SCRIPT),
|
|
67
|
+
agents: config.agents || {},
|
|
68
|
+
codexInstalled: installed.codex,
|
|
69
|
+
opencodeInstalled: installed.opencode,
|
|
70
|
+
workbuddyInstalled: installed.workbuddy,
|
|
71
|
+
observerRunning: observer.isRunning(MIO_HOME),
|
|
72
|
+
observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
|
|
73
|
+
}
|
|
71
74
|
if (useJson) {
|
|
72
75
|
jsonOrText(payload, true)
|
|
73
76
|
return
|
|
74
77
|
}
|
|
75
|
-
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
76
|
-
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
77
|
-
console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
|
|
78
|
-
console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
|
|
79
|
-
console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
|
|
80
|
-
}
|
|
78
|
+
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
79
|
+
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
80
|
+
console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
|
|
81
|
+
console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
|
|
82
|
+
console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
|
|
83
|
+
console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
|
|
84
|
+
}
|
|
81
85
|
|
|
82
86
|
function agentsCommand(useJson) {
|
|
83
87
|
const config = readConfig()
|
|
@@ -131,17 +135,69 @@ function installCommand(host, useJson) {
|
|
|
131
135
|
}
|
|
132
136
|
writeConfig(config)
|
|
133
137
|
}
|
|
138
|
+
try {
|
|
139
|
+
observer.runCycle(MIO_HOME, observer.loadState(MIO_HOME), observer.createSinks(MIO_HOME))
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
let observerStarted = false
|
|
142
|
+
try {
|
|
143
|
+
observerStarted = observer.startBackground(MIO_HOME).started
|
|
144
|
+
} catch (_) {}
|
|
134
145
|
if (useJson) {
|
|
146
|
+
result.observer = {
|
|
147
|
+
running: observer.isRunning(MIO_HOME),
|
|
148
|
+
started: observerStarted,
|
|
149
|
+
}
|
|
135
150
|
jsonOrText(result, true)
|
|
136
151
|
return
|
|
137
152
|
}
|
|
138
153
|
console.log(result.message || `${host} adapter completed.`)
|
|
154
|
+
console.log(
|
|
155
|
+
observer.isRunning(MIO_HOME)
|
|
156
|
+
? 'Observer daemon running.'
|
|
157
|
+
: 'Observer daemon not running. Run `mio observe --start`.'
|
|
158
|
+
)
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
function mcpCommand() {
|
|
142
162
|
require(SERVER_SCRIPT)
|
|
143
163
|
}
|
|
144
164
|
|
|
165
|
+
function observeCommand(args) {
|
|
166
|
+
const flags = args.slice(1)
|
|
167
|
+
const useJson = flags.includes('--json')
|
|
168
|
+
if (flags.includes('--start')) {
|
|
169
|
+
const result = observer.startBackground(MIO_HOME)
|
|
170
|
+
if (useJson) return jsonOrText(result, true)
|
|
171
|
+
console.log(result.started ? 'Observer started.' : 'Observer already running.')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (flags.includes('--stop')) {
|
|
175
|
+
const result = observer.stopBackground(MIO_HOME)
|
|
176
|
+
if (useJson) return jsonOrText(result, true)
|
|
177
|
+
console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (flags.includes('--status')) {
|
|
181
|
+
const running = observer.isRunning(MIO_HOME)
|
|
182
|
+
const pid = observer.readPid(MIO_HOME)
|
|
183
|
+
if (useJson) return jsonOrText({ running, pid }, true)
|
|
184
|
+
console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if (flags.includes('--once')) {
|
|
188
|
+
const res = observer.runCycle(
|
|
189
|
+
MIO_HOME,
|
|
190
|
+
observer.loadState(MIO_HOME),
|
|
191
|
+
observer.createSinks(MIO_HOME)
|
|
192
|
+
)
|
|
193
|
+
if (useJson) return jsonOrText(res, true)
|
|
194
|
+
console.log('Observed files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
console.log('Observing WorkBuddy transcripts into ' + MIO_HOME + ' (Ctrl+C to stop)...')
|
|
198
|
+
observer.startForeground(MIO_HOME)
|
|
199
|
+
}
|
|
200
|
+
|
|
145
201
|
function help() {
|
|
146
202
|
console.log(`Mio Agent Runtime CLI
|
|
147
203
|
|
|
@@ -151,6 +207,8 @@ Usage:
|
|
|
151
207
|
mio install <host> Install Mio into a host (codex|opencode|workbuddy)
|
|
152
208
|
mio status Show runtime and adapter status
|
|
153
209
|
mio agents List installed agents
|
|
210
|
+
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
|
|
211
|
+
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
154
212
|
mio --json status Machine-readable status
|
|
155
213
|
mio --json agents Machine-readable agents
|
|
156
214
|
`)
|
|
@@ -174,6 +232,8 @@ function main() {
|
|
|
174
232
|
return statusCommand(useJson)
|
|
175
233
|
case 'agents':
|
|
176
234
|
return agentsCommand(useJson)
|
|
235
|
+
case 'observe':
|
|
236
|
+
return observeCommand(args)
|
|
177
237
|
case 'help':
|
|
178
238
|
case '--help':
|
|
179
239
|
return help()
|
|
@@ -184,4 +244,4 @@ function main() {
|
|
|
184
244
|
}
|
|
185
245
|
}
|
|
186
246
|
|
|
187
|
-
main()
|
|
247
|
+
main()
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const crypto = require('crypto')
|
|
8
|
+
const { spawn } = require('child_process')
|
|
9
|
+
|
|
10
|
+
const INTERVAL_MS = 3000
|
|
11
|
+
// A WorkBuddy turn is considered finished when its transcript file has not
|
|
12
|
+
// been appended for this long AND an assistant answer is present.
|
|
13
|
+
const IDLE_MS = 15000
|
|
14
|
+
|
|
15
|
+
function homeDir() {
|
|
16
|
+
return process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function workbuddyConfigDir() {
|
|
20
|
+
return (
|
|
21
|
+
process.env.WORKBUDDY_CONFIG_DIR ||
|
|
22
|
+
process.env.CODEBUDDY_CONFIG_DIR ||
|
|
23
|
+
path.join(os.homedir(), '.workbuddy')
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createId(prefix) {
|
|
28
|
+
return prefix + '_' + Date.now() + '_' + crypto.randomBytes(6).toString('hex')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanText(text) {
|
|
32
|
+
return String(text || '')
|
|
33
|
+
.replace(/\r/g, '')
|
|
34
|
+
.replace(/#{2,}|\*{2,}|`{1,}|_{2,}|~{2,}|-{2,}/g, ' ')
|
|
35
|
+
.replace(/\s{2,}/g, ' ')
|
|
36
|
+
.trim()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function projectFromDirName(dirName) {
|
|
40
|
+
// WorkBuddy project dirs encode the workspace path with dashes, e.g.
|
|
41
|
+
// "d-work-code-douyin_store" -> "douyin_store"
|
|
42
|
+
const parts = String(dirName || '').split('-').filter(Boolean)
|
|
43
|
+
if (parts.length > 1 && /^\\d+$/.test(parts[parts.length - 1])) {
|
|
44
|
+
return dirName || 'unknown'
|
|
45
|
+
}
|
|
46
|
+
return parts.length > 1 ? parts[parts.length - 1] : dirName || 'unknown'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function log(home, message) {
|
|
50
|
+
const line = '[' + new Date().toISOString() + '] ' + message + '\n'
|
|
51
|
+
try {
|
|
52
|
+
fs.mkdirSync(path.join(home, 'logs'), { recursive: true })
|
|
53
|
+
fs.appendFileSync(path.join(home, 'logs', 'observe.log'), line, 'utf8')
|
|
54
|
+
} catch (_) {}
|
|
55
|
+
if (process.stdout.isTTY || process.env.MIO_OBSERVE_VERBOSE) {
|
|
56
|
+
process.stdout.write(line)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// state
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function stateFile(home) {
|
|
65
|
+
return path.join(home, 'observe-state.json')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadState(home) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(fs.readFileSync(stateFile(home), 'utf8'))
|
|
71
|
+
if (parsed && typeof parsed === 'object') {
|
|
72
|
+
return {
|
|
73
|
+
version: 1,
|
|
74
|
+
files: parsed.files || {},
|
|
75
|
+
buffers: parsed.buffers || {},
|
|
76
|
+
openTurns: parsed.openTurns || {},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
return { version: 1, files: {}, buffers: {}, openTurns: {} }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function saveState(home, state) {
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(home, { recursive: true })
|
|
86
|
+
fs.writeFileSync(stateFile(home), JSON.stringify(state) + '\n', 'utf8')
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listTranscripts() {
|
|
91
|
+
const root = path.join(workbuddyConfigDir(), 'projects')
|
|
92
|
+
const out = []
|
|
93
|
+
if (!fs.existsSync(root)) return out
|
|
94
|
+
const walk = (dir) => {
|
|
95
|
+
let entries
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
98
|
+
} catch (_) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const full = path.join(dir, entry.name)
|
|
103
|
+
if (entry.isDirectory()) walk(full)
|
|
104
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
walk(root)
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fileMeta(file) {
|
|
112
|
+
const dirName = path.basename(path.dirname(file))
|
|
113
|
+
return {
|
|
114
|
+
sessionId: path.basename(file, '.jsonl'),
|
|
115
|
+
project: projectFromDirName(dirName),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// sinks (write into MIO_HOME with the same schema as the MCP server)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function appendJsonl(file, value) {
|
|
124
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
125
|
+
fs.appendFileSync(file, JSON.stringify(value) + '\n', 'utf8')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createSinks(home) {
|
|
129
|
+
const tracePath = path.join(home, 'traces.jsonl')
|
|
130
|
+
const memoryPath = path.join(home, 'memory.jsonl')
|
|
131
|
+
const seenTraceIds = new Set()
|
|
132
|
+
const seenMemoryContents = new Set()
|
|
133
|
+
|
|
134
|
+
function trace(event) {
|
|
135
|
+
const record = {
|
|
136
|
+
id: createId('trace'),
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
trace_id: event.trace_id,
|
|
139
|
+
event_type: event.event_type,
|
|
140
|
+
outcome: event.outcome || null,
|
|
141
|
+
payload: event.payload || {},
|
|
142
|
+
agent: event.agent || 'workbuddy-observer',
|
|
143
|
+
host: event.host || 'workbuddy-observer',
|
|
144
|
+
project: event.project || 'unknown',
|
|
145
|
+
}
|
|
146
|
+
if (seenTraceIds.has(record.trace_id)) return
|
|
147
|
+
seenTraceIds.add(record.trace_id)
|
|
148
|
+
if (seenTraceIds.size > 20000) seenTraceIds.clear()
|
|
149
|
+
appendJsonl(tracePath, record)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function memory(entry) {
|
|
153
|
+
const key = entry.project + '|' + entry.content
|
|
154
|
+
if (seenMemoryContents.has(key)) return
|
|
155
|
+
seenMemoryContents.add(key)
|
|
156
|
+
const record = {
|
|
157
|
+
id: createId('mem'),
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
kind: entry.kind || 'note',
|
|
160
|
+
content: entry.content,
|
|
161
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
162
|
+
project: entry.project || 'unknown',
|
|
163
|
+
source: entry.source || 'workbuddy-observer',
|
|
164
|
+
}
|
|
165
|
+
appendJsonl(memoryPath, record)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { trace, memory }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// WorkBuddy transcript turn machine
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
function createTurn(meta) {
|
|
176
|
+
return {
|
|
177
|
+
id: null,
|
|
178
|
+
ts: null,
|
|
179
|
+
project: meta.project,
|
|
180
|
+
sessionId: meta.sessionId,
|
|
181
|
+
toolCount: 0,
|
|
182
|
+
errors: [],
|
|
183
|
+
lastAssistant: null,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function feedWorkbuddyLine(turn, obj) {
|
|
188
|
+
if (!obj || typeof obj !== 'object') return { finalized: null, next: turn }
|
|
189
|
+
let finalized = null
|
|
190
|
+
if (obj.type === 'message') {
|
|
191
|
+
if (obj.role === 'user') {
|
|
192
|
+
finalized = turn
|
|
193
|
+
const next = createTurn(turn)
|
|
194
|
+
next.id = obj.id
|
|
195
|
+
next.ts = obj.timestamp
|
|
196
|
+
return { finalized, next: next }
|
|
197
|
+
}
|
|
198
|
+
if (obj.role === 'assistant') {
|
|
199
|
+
const text = (Array.isArray(obj.content) ? obj.content : [])
|
|
200
|
+
.map((c) => (c && typeof c.text === 'string' ? c.text : ''))
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.join('\n')
|
|
203
|
+
turn.lastAssistant = {
|
|
204
|
+
status: obj.status,
|
|
205
|
+
text: text,
|
|
206
|
+
error: (obj.providerData && obj.providerData.error) || null,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else if (obj.type === 'function_call') {
|
|
210
|
+
turn.toolCount += 1
|
|
211
|
+
} else if (obj.type === 'function_call_result') {
|
|
212
|
+
const status = String(obj.status || (obj.is_error ? 'error' : 'completed'))
|
|
213
|
+
if (status !== 'completed') {
|
|
214
|
+
turn.errors.push({ callId: obj.callId, name: obj.name, status: status })
|
|
215
|
+
if (turn.errors.length > 20) turn.errors.shift()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { finalized: finalized, next: turn }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function emitWorkbuddyOutcome(turn, sinks) {
|
|
222
|
+
const la = turn.lastAssistant
|
|
223
|
+
if (!la) return
|
|
224
|
+
let outcome
|
|
225
|
+
if (la.status === 'completed') outcome = 'success'
|
|
226
|
+
else if (la.error) outcome = 'failure'
|
|
227
|
+
else outcome = 'aborted'
|
|
228
|
+
|
|
229
|
+
const summary = cleanText(la.text)
|
|
230
|
+
const turnToken = String(turn.id || 'unknown').slice(0, 8)
|
|
231
|
+
const tsToken = turn.ts ? Math.floor(Number(turn.ts) / 1000) : Math.floor(Date.now() / 1000)
|
|
232
|
+
|
|
233
|
+
sinks.trace({
|
|
234
|
+
trace_id: 'workbuddy:' + turn.sessionId + ':' + turnToken + ':' + tsToken,
|
|
235
|
+
event_type: 'task_outcome',
|
|
236
|
+
outcome: outcome,
|
|
237
|
+
payload: {
|
|
238
|
+
host: 'workbuddy',
|
|
239
|
+
project: turn.project,
|
|
240
|
+
sessionId: turn.sessionId,
|
|
241
|
+
turnId: turn.id,
|
|
242
|
+
toolCalls: turn.toolCount,
|
|
243
|
+
errors: turn.errors.length,
|
|
244
|
+
summary: summary.slice(0, 300),
|
|
245
|
+
},
|
|
246
|
+
agent: 'workbuddy',
|
|
247
|
+
host: 'workbuddy',
|
|
248
|
+
project: turn.project,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
if (outcome === 'success' && summary.length >= 40) {
|
|
252
|
+
sinks.memory({
|
|
253
|
+
kind: 'note',
|
|
254
|
+
content: '[workbuddy] 任务完成: ' + summary.slice(0, 160),
|
|
255
|
+
tags: ['task-outcome', 'workbuddy', turn.project],
|
|
256
|
+
project: turn.project,
|
|
257
|
+
source: 'workbuddy-observer',
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const err of turn.errors) {
|
|
262
|
+
sinks.trace({
|
|
263
|
+
trace_id: 'workbuddy:' + turn.sessionId + ':' + turnToken + ':' + String(err.callId || 'err').slice(0, 12),
|
|
264
|
+
event_type: 'error',
|
|
265
|
+
outcome: 'error',
|
|
266
|
+
payload: {
|
|
267
|
+
host: 'workbuddy',
|
|
268
|
+
project: turn.project,
|
|
269
|
+
sessionId: turn.sessionId,
|
|
270
|
+
turnId: turn.id,
|
|
271
|
+
tool: err.name,
|
|
272
|
+
status: err.status,
|
|
273
|
+
},
|
|
274
|
+
agent: 'workbuddy',
|
|
275
|
+
host: 'workbuddy',
|
|
276
|
+
project: turn.project,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// cycle
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function runCycle(home, state, sinks, opts) {
|
|
286
|
+
const out = { files: 0, lines: 0, outcomes: 0 }
|
|
287
|
+
fs.mkdirSync(home, { recursive: true })
|
|
288
|
+
const files = listTranscripts()
|
|
289
|
+
const now = Date.now()
|
|
290
|
+
const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
|
|
291
|
+
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
out.files += 1
|
|
294
|
+
let st
|
|
295
|
+
try {
|
|
296
|
+
st = fs.statSync(file)
|
|
297
|
+
} catch (_) {
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
const info = state.files[file] || { size: 0 }
|
|
301
|
+
// Do not flood MIO_HOME with ancient history: only backfill files that are
|
|
302
|
+
// already tracked or that changed within the max age window.
|
|
303
|
+
if (!state.files[file] && now - st.mtimeMs > maxAgeMs) continue
|
|
304
|
+
const size = st.size
|
|
305
|
+
if (size < info.size) {
|
|
306
|
+
// rotated/truncated -> restart from the beginning
|
|
307
|
+
info.size = 0
|
|
308
|
+
state.buffers[file] = ''
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const meta = fileMeta(file)
|
|
312
|
+
let turn = state.openTurns[file] || createTurn(meta)
|
|
313
|
+
turn.project = meta.project
|
|
314
|
+
turn.sessionId = meta.sessionId
|
|
315
|
+
|
|
316
|
+
if (size > info.size) {
|
|
317
|
+
let chunk = ''
|
|
318
|
+
try {
|
|
319
|
+
const fd = fs.openSync(file, 'r')
|
|
320
|
+
const buf = Buffer.alloc(size - info.size)
|
|
321
|
+
fs.readSync(fd, buf, 0, buf.length, info.size)
|
|
322
|
+
fs.closeSync(fd)
|
|
323
|
+
chunk = buf.toString('utf8')
|
|
324
|
+
} catch (_) {
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
const pending = (state.buffers[file] || '') + chunk
|
|
328
|
+
const lines = pending.split(/\r?\n/)
|
|
329
|
+
state.buffers[file] = lines.pop()
|
|
330
|
+
for (const line of lines) {
|
|
331
|
+
if (!line.trim()) continue
|
|
332
|
+
out.lines += 1
|
|
333
|
+
let obj
|
|
334
|
+
try {
|
|
335
|
+
obj = JSON.parse(line)
|
|
336
|
+
} catch (_) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
const result = feedWorkbuddyLine(turn, obj)
|
|
340
|
+
if (result.finalized) {
|
|
341
|
+
emitWorkbuddyOutcome(result.finalized, sinks)
|
|
342
|
+
out.outcomes += 1
|
|
343
|
+
}
|
|
344
|
+
turn = result.next
|
|
345
|
+
}
|
|
346
|
+
state.files[file] = { size: size, mtimeMs: st.mtimeMs }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (turn.lastAssistant && now - st.mtimeMs > IDLE_MS) {
|
|
350
|
+
emitWorkbuddyOutcome(turn, sinks)
|
|
351
|
+
out.outcomes += 1
|
|
352
|
+
state.openTurns[file] = null
|
|
353
|
+
} else if (turn.id) {
|
|
354
|
+
state.openTurns[file] = turn
|
|
355
|
+
} else {
|
|
356
|
+
state.openTurns[file] = null
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
saveState(home, state)
|
|
361
|
+
return out
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function startForeground(home) {
|
|
365
|
+
const state = loadState(home)
|
|
366
|
+
const sinks = createSinks(home)
|
|
367
|
+
log(home, 'observe started (home=' + home + ', interval=' + INTERVAL_MS + 'ms)')
|
|
368
|
+
const tick = () => {
|
|
369
|
+
try {
|
|
370
|
+
const res = runCycle(home, state, sinks)
|
|
371
|
+
if (res.lines > 0) {
|
|
372
|
+
log(home, 'cycle: files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log(home, 'cycle error: ' + (err && err.stack ? err.stack : String(err)))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
tick()
|
|
379
|
+
setInterval(tick, INTERVAL_MS)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// background daemon control
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
function pidFile(home) {
|
|
387
|
+
return path.join(home, 'observe.pid')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readPid(home) {
|
|
391
|
+
try {
|
|
392
|
+
const pid = Number(fs.readFileSync(pidFile(home), 'utf8').trim())
|
|
393
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null
|
|
394
|
+
} catch (_) {
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isRunning(home) {
|
|
400
|
+
const pid = readPid(home)
|
|
401
|
+
if (!pid) return false
|
|
402
|
+
try {
|
|
403
|
+
process.kill(pid, 0)
|
|
404
|
+
return true
|
|
405
|
+
} catch (_) {
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function startBackground(home) {
|
|
411
|
+
if (isRunning(home)) {
|
|
412
|
+
return { running: true, started: false, pid: readPid(home) }
|
|
413
|
+
}
|
|
414
|
+
const script = path.join(__dirname, '..', 'bin', 'mio.js')
|
|
415
|
+
const child = spawn(process.execPath, [script, 'observe'], {
|
|
416
|
+
detached: true,
|
|
417
|
+
stdio: 'ignore',
|
|
418
|
+
windowsHide: true,
|
|
419
|
+
})
|
|
420
|
+
child.unref()
|
|
421
|
+
fs.mkdirSync(home, { recursive: true })
|
|
422
|
+
fs.writeFileSync(pidFile(home), String(child.pid), 'utf8')
|
|
423
|
+
return { running: true, started: true, pid: child.pid }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function stopBackground(home) {
|
|
427
|
+
const pid = readPid(home)
|
|
428
|
+
if (pid) {
|
|
429
|
+
try {
|
|
430
|
+
process.kill(pid)
|
|
431
|
+
} catch (_) {}
|
|
432
|
+
try {
|
|
433
|
+
fs.unlinkSync(pidFile(home))
|
|
434
|
+
} catch (_) {}
|
|
435
|
+
}
|
|
436
|
+
return { running: false, stopped: Boolean(pid), pid: pid || null }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
module.exports = {
|
|
440
|
+
INTERVAL_MS,
|
|
441
|
+
homeDir,
|
|
442
|
+
workbuddyConfigDir,
|
|
443
|
+
loadState,
|
|
444
|
+
saveState,
|
|
445
|
+
runCycle,
|
|
446
|
+
createSinks,
|
|
447
|
+
startForeground,
|
|
448
|
+
startBackground,
|
|
449
|
+
stopBackground,
|
|
450
|
+
isRunning,
|
|
451
|
+
readPid,
|
|
452
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"server/",
|
|
13
13
|
"adapters/",
|
|
14
|
+
"observe/",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
|
-
"check": "node --check bin/mio.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check server/mio-intelligence-mcp/index.js",
|
|
18
|
+
"check": "node --check bin/mio.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check server/mio-intelligence-mcp/index.js && node --check observe/observer.js",
|
|
18
19
|
"prepack": "npm run check"
|
|
19
20
|
},
|
|
20
21
|
"engines": {
|