mio-agent-runtime 0.3.5 → 0.3.7
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 +12 -3
- package/adapters/hermes.js +90 -0
- package/bin/mio.js +253 -247
- package/observe/observer.js +237 -0
- package/package.json +4 -3
- package/server/mio-intelligence-mcp/README.md +174 -0
- package/server/mio-intelligence-mcp/index.js +1179 -27
- package/server/mio-intelligence-mcp/package.json +18 -0
- package/server/mio-intelligence-mcp/phase0.js +381 -0
package/observe/observer.js
CHANGED
|
@@ -83,6 +83,9 @@ function loadState(home) {
|
|
|
83
83
|
ocDbMtime: parsed.ocDbMtime || null,
|
|
84
84
|
ocPending: parsed.ocPending || false,
|
|
85
85
|
ocTurns: parsed.ocTurns || {},
|
|
86
|
+
hmSeeded: parsed.hmSeeded || false,
|
|
87
|
+
hmLastMsgId: parsed.hmLastMsgId || 0,
|
|
88
|
+
hmTurns: parsed.hmTurns || {},
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
} catch (_) {}
|
|
@@ -100,6 +103,9 @@ function loadState(home) {
|
|
|
100
103
|
ocDbMtime: null,
|
|
101
104
|
ocPending: false,
|
|
102
105
|
ocTurns: {},
|
|
106
|
+
hmSeeded: false,
|
|
107
|
+
hmLastMsgId: 0,
|
|
108
|
+
hmTurns: {},
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -883,6 +889,230 @@ function runOpencodeCycle(home, state, sinks, opts) {
|
|
|
883
889
|
return out
|
|
884
890
|
}
|
|
885
891
|
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Hermes session observation (passive, via its own SQLite state.db)
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
// Hermes (Nous Research hermes-agent) keeps every conversation in
|
|
897
|
+
// <LocalAppData>/hermes/state.db. Unlike OpenCode the file is not exclusively
|
|
898
|
+
// locked, and `messages.id` is a monotonic AUTOINCREMENT cursor, so we can tail
|
|
899
|
+
// it read-only with node:sqlite (Node 22+). Each message is one row carrying
|
|
900
|
+
// role / content / finish_reason, so a user row opens a turn and an assistant
|
|
901
|
+
// row with a terminal finish_reason closes it immediately. Hermes already has
|
|
902
|
+
// native memory, so this observer only imports Hermes experience into the Mio
|
|
903
|
+
// ecosystem (traces + memory + AGENTS.md context when a git repo is present).
|
|
904
|
+
|
|
905
|
+
let _hermesSqlite = null
|
|
906
|
+
let _hermesSqliteTried = false
|
|
907
|
+
|
|
908
|
+
function hermesSqliteModule() {
|
|
909
|
+
if (_hermesSqliteTried) return _hermesSqlite
|
|
910
|
+
_hermesSqliteTried = true
|
|
911
|
+
try {
|
|
912
|
+
_hermesSqlite = require('node:sqlite')
|
|
913
|
+
} catch (_) {
|
|
914
|
+
_hermesSqlite = null
|
|
915
|
+
}
|
|
916
|
+
return _hermesSqlite
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function hermesDbPath() {
|
|
920
|
+
return (
|
|
921
|
+
process.env.HERMES_DATA_DIR ||
|
|
922
|
+
path.join(os.homedir(), 'AppData', 'Local', 'hermes', 'state.db')
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function hermesProjectName(meta) {
|
|
927
|
+
if (meta.git) return path.basename(meta.git)
|
|
928
|
+
if (meta.cwd && meta.cwd !== os.homedir()) return path.basename(meta.cwd)
|
|
929
|
+
return 'hermes'
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function createHermesTurn(meta) {
|
|
933
|
+
meta = meta || {}
|
|
934
|
+
return {
|
|
935
|
+
id: null,
|
|
936
|
+
ts: null,
|
|
937
|
+
sessionId: meta.sessionId || null,
|
|
938
|
+
cwd: meta.git || null,
|
|
939
|
+
project: hermesProjectName({ git: meta.git, cwd: meta.projectHint }),
|
|
940
|
+
title: meta.title || null,
|
|
941
|
+
agent: meta.agent || null,
|
|
942
|
+
lastText: null,
|
|
943
|
+
toolCount: 0,
|
|
944
|
+
lastMid: null,
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function hermesOutcomeFor(finish) {
|
|
949
|
+
if (finish === 'error' || finish === 'failure') return 'failure'
|
|
950
|
+
if (finish === 'cancel' || finish === 'aborted' || finish === 'interrupted') return 'aborted'
|
|
951
|
+
return 'success'
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function finalizeHermesTurn(turn, outcome) {
|
|
955
|
+
if (!turn.id) return null
|
|
956
|
+
const finalized = {
|
|
957
|
+
id: turn.id,
|
|
958
|
+
ts: turn.ts,
|
|
959
|
+
sessionId: turn.sessionId,
|
|
960
|
+
cwd: turn.cwd,
|
|
961
|
+
project: turn.project,
|
|
962
|
+
agent: turn.agent,
|
|
963
|
+
title: turn.title,
|
|
964
|
+
lastText: turn.lastText,
|
|
965
|
+
toolCount: turn.toolCount,
|
|
966
|
+
errors: [],
|
|
967
|
+
outcome: outcome,
|
|
968
|
+
}
|
|
969
|
+
return finalized
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function feedHermesRow(turn, row) {
|
|
973
|
+
const meta = { sessionId: row.sid, title: row.stitle, git: row.sgit, agent: row.ssource, projectHint: row.scwd }
|
|
974
|
+
|
|
975
|
+
if (row.role === 'user') {
|
|
976
|
+
let finalized = null
|
|
977
|
+
if (turn.id) finalized = finalizeHermesTurn(turn, 'aborted')
|
|
978
|
+
const next = createHermesTurn(meta)
|
|
979
|
+
next.id = Number(row.id)
|
|
980
|
+
next.ts = Math.floor(Number(row.t) * 1000) || Date.now()
|
|
981
|
+
next.lastMid = Number(row.id)
|
|
982
|
+
return { finalized, next }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (row.role === 'assistant') {
|
|
986
|
+
if (!turn.id) return { next: turn }
|
|
987
|
+
if (typeof row.content === 'string' && row.content.trim()) {
|
|
988
|
+
turn.lastText = row.content
|
|
989
|
+
}
|
|
990
|
+
if (row.tool_calls) {
|
|
991
|
+
try {
|
|
992
|
+
const calls = JSON.parse(row.tool_calls)
|
|
993
|
+
if (Array.isArray(calls)) turn.toolCount += calls.length
|
|
994
|
+
} catch (_) {}
|
|
995
|
+
}
|
|
996
|
+
if (row.finish_reason && row.finish_reason !== 'tool_calls') {
|
|
997
|
+
const finalized = finalizeHermesTurn(turn, hermesOutcomeFor(row.finish_reason))
|
|
998
|
+
return { finalized, next: createHermesTurn(meta) }
|
|
999
|
+
}
|
|
1000
|
+
turn.lastMid = Number(row.id)
|
|
1001
|
+
return { next: turn }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return { next: turn }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function emitHermesOutcome(turn, sinks, state) {
|
|
1008
|
+
if (!turn.id) return false
|
|
1009
|
+
const summary = cleanText(turn.lastText)
|
|
1010
|
+
if (!summary) return false
|
|
1011
|
+
const outcome = turn.outcome
|
|
1012
|
+
const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
|
|
1013
|
+
|
|
1014
|
+
sinks.trace({
|
|
1015
|
+
trace_id: 'hermes:' + (turn.sessionId || 'unknown') + ':' + String(turn.id) + ':' + tsToken,
|
|
1016
|
+
event_type: 'task_outcome',
|
|
1017
|
+
outcome: outcome,
|
|
1018
|
+
payload: {
|
|
1019
|
+
host: 'hermes',
|
|
1020
|
+
project: turn.project,
|
|
1021
|
+
sessionId: turn.sessionId,
|
|
1022
|
+
turnId: turn.id,
|
|
1023
|
+
title: turn.title,
|
|
1024
|
+
agent: turn.agent,
|
|
1025
|
+
toolCalls: turn.toolCount,
|
|
1026
|
+
errors: 0,
|
|
1027
|
+
summary: summary.slice(0, 300),
|
|
1028
|
+
source: 'hermes-observer',
|
|
1029
|
+
},
|
|
1030
|
+
agent: 'hermes',
|
|
1031
|
+
host: 'hermes',
|
|
1032
|
+
project: turn.project,
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
if (outcome === 'success' && summary.length >= 10) {
|
|
1036
|
+
sinks.memory({
|
|
1037
|
+
kind: 'note',
|
|
1038
|
+
content: '[hermes] 任务完成: ' + summary.slice(0, 160),
|
|
1039
|
+
tags: ['task-outcome', 'hermes', turn.project],
|
|
1040
|
+
project: turn.project,
|
|
1041
|
+
source: 'hermes-observer',
|
|
1042
|
+
})
|
|
1043
|
+
// Hermes reads AGENTS.md Context Files; write back only into a real repo
|
|
1044
|
+
// workspace so we never pollute the user's home directory.
|
|
1045
|
+
if (turn.cwd) {
|
|
1046
|
+
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
1047
|
+
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return true
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function runHermesCycle(home, state, sinks, opts) {
|
|
1054
|
+
const out = { files: 0, lines: 0, outcomes: 0 }
|
|
1055
|
+
const dbPath = hermesDbPath()
|
|
1056
|
+
if (!fs.existsSync(dbPath)) return out
|
|
1057
|
+
const sqlite = hermesSqliteModule()
|
|
1058
|
+
if (!sqlite) return out
|
|
1059
|
+
let db
|
|
1060
|
+
try {
|
|
1061
|
+
db = new sqlite.DatabaseSync(dbPath, { readOnly: true })
|
|
1062
|
+
} catch (_) {
|
|
1063
|
+
return out
|
|
1064
|
+
}
|
|
1065
|
+
out.files = 1
|
|
1066
|
+
const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
|
|
1067
|
+
try {
|
|
1068
|
+
if (!state.hmSeeded) {
|
|
1069
|
+
const cutoff = (Date.now() - maxAgeMs) / 1000
|
|
1070
|
+
const first = db.prepare('SELECT id FROM messages WHERE active = 1 AND timestamp >= ? ORDER BY id LIMIT 1').get(cutoff)
|
|
1071
|
+
if (first) {
|
|
1072
|
+
state.hmLastMsgId = first.id
|
|
1073
|
+
} else {
|
|
1074
|
+
const maxRow = db.prepare('SELECT COALESCE(MAX(id), 0) AS id FROM messages').get()
|
|
1075
|
+
state.hmLastMsgId = maxRow.id
|
|
1076
|
+
}
|
|
1077
|
+
state.hmSeeded = true
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const limit = 2000
|
|
1081
|
+
const stmt = db.prepare(
|
|
1082
|
+
'SELECT m.id AS id, m.session_id AS sid, m.role AS role, m.content AS content, ' +
|
|
1083
|
+
'm.tool_name AS tool_name, m.tool_calls AS tool_calls, m.timestamp AS t, m.finish_reason AS finish_reason, ' +
|
|
1084
|
+
's.title AS stitle, s.cwd AS scwd, s.git_repo_root AS sgit, s.source AS ssource ' +
|
|
1085
|
+
'FROM messages m LEFT JOIN sessions s ON s.id = m.session_id ' +
|
|
1086
|
+
'WHERE m.id > ? AND m.active = 1 ORDER BY m.id LIMIT ' + limit
|
|
1087
|
+
)
|
|
1088
|
+
const rows = stmt.all(state.hmLastMsgId)
|
|
1089
|
+
if (rows.length === 0) return out
|
|
1090
|
+
|
|
1091
|
+
let lastId = state.hmLastMsgId
|
|
1092
|
+
for (const row of rows) {
|
|
1093
|
+
const rid = Number(row.id)
|
|
1094
|
+
if (rid > lastId) lastId = rid
|
|
1095
|
+
out.lines += 1
|
|
1096
|
+
const sid = row.sid
|
|
1097
|
+
let turn = state.hmTurns[sid]
|
|
1098
|
+
if (!turn) {
|
|
1099
|
+
turn = createHermesTurn({ sessionId: sid, title: row.stitle, git: row.sgit, agent: row.ssource, projectHint: row.scwd })
|
|
1100
|
+
state.hmTurns[sid] = turn
|
|
1101
|
+
}
|
|
1102
|
+
const result = feedHermesRow(turn, row)
|
|
1103
|
+
if (result.finalized) {
|
|
1104
|
+
if (emitHermesOutcome(result.finalized, sinks, state)) out.outcomes += 1
|
|
1105
|
+
}
|
|
1106
|
+
state.hmTurns[sid] = result.next
|
|
1107
|
+
}
|
|
1108
|
+
state.hmLastMsgId = lastId
|
|
1109
|
+
} finally {
|
|
1110
|
+
db.close()
|
|
1111
|
+
}
|
|
1112
|
+
return out
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
|
|
886
1116
|
// ---------------------------------------------------------------------------
|
|
887
1117
|
// cycle
|
|
888
1118
|
// ---------------------------------------------------------------------------
|
|
@@ -970,6 +1200,11 @@ function runCycle(home, state, sinks, opts) {
|
|
|
970
1200
|
out.lines += ocRes.lines
|
|
971
1201
|
out.outcomes += ocRes.outcomes
|
|
972
1202
|
|
|
1203
|
+
const hmRes = runHermesCycle(home, state, sinks, opts)
|
|
1204
|
+
out.files += hmRes.files
|
|
1205
|
+
out.lines += hmRes.lines
|
|
1206
|
+
out.outcomes += hmRes.outcomes
|
|
1207
|
+
|
|
973
1208
|
saveState(home, state)
|
|
974
1209
|
return out
|
|
975
1210
|
}
|
|
@@ -1059,6 +1294,8 @@ module.exports = {
|
|
|
1059
1294
|
runCycle,
|
|
1060
1295
|
opencodeDbPath,
|
|
1061
1296
|
runOpencodeCycle,
|
|
1297
|
+
runHermesCycle,
|
|
1298
|
+
hermesDbPath,
|
|
1062
1299
|
createSinks,
|
|
1063
1300
|
startForeground,
|
|
1064
1301
|
startBackground,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
"server/",
|
|
13
13
|
"adapters/",
|
|
14
14
|
"observe/",
|
|
15
|
-
"README.md"
|
|
15
|
+
"README.md",
|
|
16
|
+
"!server/mio-intelligence-mcp/__tests__"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
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",
|
|
19
|
+
"check": "node --check bin/mio.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js",
|
|
19
20
|
"prepack": "npm run check"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# mio-intelligence-mcp
|
|
2
|
+
|
|
3
|
+
Mio Agent Enhancement Layer 的最小 MCP 实现。
|
|
4
|
+
|
|
5
|
+
第一阶段目标:让 Codex / OpenCode / WorkBuddy 通过 MCP 接入 Mio,积累真实 Agent 行为数据。
|
|
6
|
+
|
|
7
|
+
## 当前工具
|
|
8
|
+
|
|
9
|
+
- `mio.memory.query` — 查询本地项目记忆;支持 `kind` / `tags`(AND)过滤、`scope` 跨项目命名空间(`project` / `global` / `all`),并采用混合检索评分(词法命中 + 拉丁子串召回 + CJK 双字短语加权 + 新近度加分)。
|
|
10
|
+
- `mio.memory.record` — 记录决策、上下文或历史问题;`scope: global` 写入跨项目可复用的全局记忆(`project` 置空),默认 `scope: project`。
|
|
11
|
+
- `mio.memory.analyze` — 分析记忆质量:kind 分布、重复组(内容相似度 ≥ 0.75)、低质量记录(缺 kind/content 或内容过短),并返回维护建议。
|
|
12
|
+
- `mio.memory.archive` — 按 id 归档(或 `restore: true` 恢复)记忆记录,非破坏性:归档记录默认从 `memory.query` / `memory.analyze` 中排除,便于去重整理。
|
|
13
|
+
- `mio.memory.migrate` — 跨项目经验迁移(P3):把记录在 project / global 两层之间迁移;迁到 global 后跨项目可见(`project` 置空),迁回 project 恢复项目隔离;保留 `migratedAt` / `migratedFrom` 溯源。
|
|
14
|
+
- `mio.observer.ingest` — 接收 Agent Trace / tool call / error / retry / outcome。
|
|
15
|
+
- `mio.policy.check` — 根据历史 trace 结果,为即将执行的操作返回风险建议(`riskLevel`、outcome 分布、最近失败证据 `failureExamples`)与执行指导 `guidance`:无历史=`none` / 有证据=`advisory` / 有 verified 经验=`actionable`(含安全替代方案与验证步骤),始终不设硬门禁。
|
|
16
|
+
- `mio.observer.subscribe` — 订阅观察事件(项目、事件类型、可选 topic 关键词),订阅有效期内可被 `mio.observer.digest` 拉取。
|
|
17
|
+
- `mio.observer.digest` — 拉取自上次 digest 以来匹配订阅的新事件(游标自动推进),实现 pull 式事件订阅。
|
|
18
|
+
- `mio.experience.reuse` — 记录 Phase 0 跨 Agent 经验复用的验证证据;每次记录后同时返回当前 Phase 0 剩余门槛。
|
|
19
|
+
- `mio.experience.confirm` — 确认一条 `source: auto_claim` 的复用证据为真实复用(`behaviorChanged` 置为 true 并标记 `confirmed`),使记录计入 verified reuse 门槛。
|
|
20
|
+
- `mio.experience.list` — 列出 Phase 0 复用证据,可按 `status`(`pending` / `confirmed` / `verified` / `auto_claim` / `agent_report` / `all`)、`targetAgent`、`project` 过滤,默认返回最近 20 条。
|
|
21
|
+
- `mio.phase0.report` — 生成当前 Phase 0 验证报告,返回门槛状态、复用漏斗(含 `pendingAutoClaims`,即待确认的 auto_claim 数)、Agent 覆盖(含每个 host 的健康状态)、trace outcome 分布;支持 `format: markdown` 直接返回渲染报告。
|
|
22
|
+
- `mio.task.route` — 把任务路由到最相关的**已验证经验**与相关记忆(P1):对任务描述做混合检索匹配 verified 经验,返回可直接采用的模式(含 `reuseCount` 复用次数、确认状态、来源/目标 Agent 与记忆内容),把「查询响应」升级为「决策影响」;路由结果会进入查询日志,便于后续 `task_outcome` 自动关联复用证据。
|
|
23
|
+
|
|
24
|
+
## 项目识别
|
|
25
|
+
|
|
26
|
+
不传 `project` 时,服务会通过当前 Git 仓库自动识别项目名:
|
|
27
|
+
|
|
28
|
+
1. 读取 `git rev-parse --path-format=absolute --git-common-dir`
|
|
29
|
+
2. 取其父目录名作为项目名
|
|
30
|
+
3. 非 Git 目录回退到当前目录名
|
|
31
|
+
|
|
32
|
+
因此同一个仓库无论在主工作区还是 worktree 中,都会得到一致的项目名。
|
|
33
|
+
|
|
34
|
+
## 跨项目命名空间与混合检索(P0)
|
|
35
|
+
|
|
36
|
+
`memory.query` 支持 `scope`:
|
|
37
|
+
|
|
38
|
+
- `project`(默认):只搜当前项目记忆,全局记忆不可见。
|
|
39
|
+
- `global`:只搜 `scope: 'global'` 的全局记忆。
|
|
40
|
+
- `all`:当前项目记忆 + 全局记忆(其他项目仍不可见)。
|
|
41
|
+
|
|
42
|
+
`memory.record` 支持 `scope: 'global'` 写入跨项目经验(如「如何在本机配置 MCP」);全局记录 `project` 置空,防止项目上下文泄露。
|
|
43
|
+
|
|
44
|
+
混合检索评分(无需外部模型):
|
|
45
|
+
|
|
46
|
+
1. 词法命中:查询词与记录内容的精确匹配(中文按单字)。
|
|
47
|
+
2. 拉丁子串召回:`retriev` 可召回含 `retrieval` 的记录。
|
|
48
|
+
3. CJK 双字短语加权:含 `部署环境` 连续短语的记录排名更高。
|
|
49
|
+
4. 新近度加分:仅对已有内容匹配的记录生效,不放大零匹配记录。
|
|
50
|
+
|
|
51
|
+
向量/语义嵌入(真正按「含义」召回)留作后续阶段(P0.5),评分接口已预留扩展位。
|
|
52
|
+
|
|
53
|
+
## 证据加权排名(P2)
|
|
54
|
+
|
|
55
|
+
`memory.query` / `policy.check` / `task.route` 的检索排序会叠加「证据权重」:
|
|
56
|
+
|
|
57
|
+
- 来源:`experience_reuse.jsonl` 中满足 verified(reuse + behaviorChanged + outcomeImproved)的记录。
|
|
58
|
+
- 加分:`min(2, reuseCount * 0.6 + (confirmedCount > 0 ? 1 : 0))`——被成功复用过的记忆排名更高,确认过的额外 +1,封顶 2 分,保证内容相关度仍是主导。
|
|
59
|
+
- 边界:证据加分只作用于「已内容匹配」的记录,不会让零匹配记录被召回(与新近度加分同一原则)。
|
|
60
|
+
- 可见性:`memory.query` 命中带证据的记忆时,结果带 `evidence: { reuseCount, confirmedCount, lastReusedAt }`;`policy.check` 的 `related_memories` 同样带 `evidence`。
|
|
61
|
+
|
|
62
|
+
task_outcome 通过 auto-claim → confirm 沉淀为 verified 复用记录,从而进入该权重——即「任务结果反哺检索排序」的自改进闭环。
|
|
63
|
+
|
|
64
|
+
## 跨项目经验迁移与分层记忆(P3)
|
|
65
|
+
|
|
66
|
+
记忆分两层:项目级(默认,`project` 隔离)与全局级(`scope: global`,跨项目可见)。
|
|
67
|
+
|
|
68
|
+
- `mio.memory.migrate` — 把记忆在两层间迁移(`ids` + `scope: global|project`)。迁到全局后 `project` 置空、可被任意项目召回;迁回项目恢复隔离;保留 `migratedAt` / `migratedFrom` 溯源。
|
|
69
|
+
- `memory.query` 响应带 `layers: { project, global }` 计数;`scope: all` 下全局记录有 +0.5 可见性加分,防止跨项目经验被项目记录挤掉(内容相关度仍主导排序)。
|
|
70
|
+
- `memory.analyze` 现在同时分析项目与全局记录,并报告 `layers` 分层计数。
|
|
71
|
+
- 边界:迁移是显式操作,避免项目上下文意外泄露到全局层。
|
|
72
|
+
|
|
73
|
+
## Policy 执行指导(P4)
|
|
74
|
+
|
|
75
|
+
`policy.check` 在风险建议之上增加 `guidance`,把「警告」升级为「可执行指导」:
|
|
76
|
+
|
|
77
|
+
- `level`:`none`(无历史)→ `advisory`(有证据但未验证)→ `actionable`(存在 verified 经验,直接给出可采用的方案)。
|
|
78
|
+
- `saferAlternatives`:来自带证据的 related_memories(verified 复用经验),给出 `memoryId` / `reuseCount` / 内容摘要。
|
|
79
|
+
- `verificationSteps`:执行后的验证步骤与 outcome 记录建议。
|
|
80
|
+
- `avoid`:重复失败场景下的失败模式摘要(取自 `failureExamples`)。
|
|
81
|
+
- `hardGate`:恒为 `false`——P4 只做指导,不设硬门禁(确凿证据继续积累后再考虑)。
|
|
82
|
+
|
|
83
|
+
task_outcome 数据经 auto-claim → confirm 后会自动升级后续同类动作的指导级别。
|
|
84
|
+
|
|
85
|
+
## 全局 Codex 配置
|
|
86
|
+
|
|
87
|
+
`C:\Users\gf191\.codex\config.toml` 中配置:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
[mcp_servers.mio-intelligence]
|
|
91
|
+
command = 'C:\nvm4w\nodejs\node.exe'
|
|
92
|
+
args = ['D:\work\code\akemi-mio\.worktrees\mio-agent-enhancement-layer\server\mio-intelligence-mcp\index.js']
|
|
93
|
+
startup_timeout_sec = 30
|
|
94
|
+
|
|
95
|
+
[mcp_servers.mio-intelligence.env]
|
|
96
|
+
MIO_DATA_DIR = 'C:\Users\gf191\.mio-intelligence'
|
|
97
|
+
MIO_CONTEXT = '{"agentId":"codex","project":"akemi-mio","workspace":"D:/work/code/akemi-mio","sessionId":"codex-session"}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
这样所有 Codex 工作区都能调用 Mio 工具,数据统一存储在全局目录,并通过 `project` 字段隔离。
|
|
101
|
+
|
|
102
|
+
## 统一调用方身份
|
|
103
|
+
|
|
104
|
+
每个 Agent 启动 Mio MCP 时通过 `MIO_CONTEXT` 提供身份:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"agentId": "codex",
|
|
109
|
+
"project": "akemi-mio",
|
|
110
|
+
"workspace": "D:/work/code/akemi-mio",
|
|
111
|
+
"sessionId": "codex-session"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Mio 会把 `agentId` 注入记忆来源和观察事件,把 `project` 作为默认项目名。这样 Codex、OpenCode、WorkBuddy 连接同一个 Mio 服务时,经验来源可区分。
|
|
116
|
+
|
|
117
|
+
OpenCode / WorkBuddy 的 MCP 配置只需保持同样语义:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"mio": {
|
|
123
|
+
"command": "node",
|
|
124
|
+
"args": [
|
|
125
|
+
"D:/work/code/akemi-mio/.worktrees/mio-agent-enhancement-layer/server/mio-intelligence-mcp/index.js"
|
|
126
|
+
],
|
|
127
|
+
"env": {
|
|
128
|
+
"MIO_DATA_DIR": "C:/Users/gf191/.mio-intelligence",
|
|
129
|
+
"MIO_CONTEXT": "{\"agentId\":\"opencode\",\"project\":\"akemi-mio\",\"workspace\":\"D:/work/code/akemi-mio\",\"sessionId\":\"opencode-session\"}"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 自动使用规则
|
|
137
|
+
|
|
138
|
+
全局规则位于 `C:\Users\gf191\.codex\AGENTS.md`。Codex 会在每个工作区执行:
|
|
139
|
+
|
|
140
|
+
- 规划前查询 `mio.memory.query`
|
|
141
|
+
- 关键决策后写入 `mio.memory.record`
|
|
142
|
+
- 报错或重试时上报 `mio.observer.ingest`
|
|
143
|
+
- 每个任务结束前上报 `mio.observer.ingest`,`event_type` 为 `task_outcome`,并填写 `outcome` 和非敏感 `payload`
|
|
144
|
+
- 高风险操作前检查 `mio.policy.check`
|
|
145
|
+
|
|
146
|
+
## 复用证据自动关联
|
|
147
|
+
|
|
148
|
+
服务端会把最近 200 次返回非空结果的 `mio.memory.query` 持久化到 `queries.jsonl`(含 `expiresAt`,过期项自动清理),因此匹配可以跨进程、跨重启生效。当同一 `agent + project` 在匹配窗口内(默认 60 分钟,可用环境变量 `MIO_REUSE_MATCH_WINDOW_MIN` 覆盖)上报 `task_outcome` 时,自动生成一条 `source: auto_claim` 的复用证据:
|
|
149
|
+
|
|
150
|
+
- `sourceAgent` 优先取被召回记忆中与执行 Agent 不同的来源(跨 Agent 证据),否则为执行 Agent 自身。
|
|
151
|
+
- `behaviorChanged` 初始为 `false`;`outcomeImproved` 取决于 outcome 是否为 `success`。
|
|
152
|
+
- 自动证据只计入 `autoClaimedReuse` / `claimedReuse`,**默认不满足** verified reuse 门槛。
|
|
153
|
+
- Agent 确认真实发生复用后,调用 `mio.experience.confirm`(参数为 auto-claim 记录的 `id`),把 `behaviorChanged` 置为 true 并标记 `confirmed: true` / `confirmedBy`;若 `outcomeImproved` 同时为 true,该记录计入 `confirmedReuse` 与 verified reuse 门槛。
|
|
154
|
+
- 也可以直接调用 `mio.experience.reuse`(三个布尔位为真)记录 `agent_report` 证据。
|
|
155
|
+
|
|
156
|
+
## Host 健康状态
|
|
157
|
+
|
|
158
|
+
`mio.phase0.report` 的 `agentCoverage` 对每个 host 返回:
|
|
159
|
+
|
|
160
|
+
- `memoryRecords` / `traceEvents` / `taskOutcomes` / `verifiedReuseAsTarget` — 该 host 的贡献计数。
|
|
161
|
+
- `lastActiveAt` / `lastActiveHoursAgo` — 最近一条证据的时间。
|
|
162
|
+
- `active` — 7 天内(`HOST_ACTIVE_WINDOW_MS`)是否有活动。
|
|
163
|
+
- `dataSources` — 该 host 出现在哪些证据文件(`memory` / `trace` / `reuse`)。
|
|
164
|
+
|
|
165
|
+
同时返回 `activeHostCount`,用于判断 “≥2 hosts” 门槛当前是否真的有多个活跃 Agent,避免“注册过但已停用”的假阳性。
|
|
166
|
+
|
|
167
|
+
## 数据文件
|
|
168
|
+
|
|
169
|
+
- `memory.jsonl` — 记忆记录。
|
|
170
|
+
- `traces.jsonl` — Agent Trace 事件。
|
|
171
|
+
- `experience_reuse.jsonl` — Phase 0 有效复用证据。
|
|
172
|
+
- `queries.jsonl` — 最近 `memory.query` 的持久化记录,用于自动关联复用证据。
|
|
173
|
+
|
|
174
|
+
均为本地 JSONL,不依赖数据库。后续可以从这里逐步替换为 Mio 现有 SQLite / MemoryService。
|