mio-agent-runtime 0.5.23 → 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.
@@ -65,26 +65,67 @@ function stateFile(home) {
65
65
  return path.join(home, 'observe-state.json')
66
66
  }
67
67
 
68
+ // Canonical form for a workspace path used as a context key.
69
+ //
70
+ // Context keys are taken verbatim from the `cwd` field of host transcripts, and
71
+ // hosts disagree on spelling: the same directory arrives as `D:\work\code\x`,
72
+ // `d:\work\code\x`, or `D:/work/code/x`. Left alone, one project splits into
73
+ // several buckets, each capped at CONTEXT_MAX, so its history is fragmented and
74
+ // entries are dropped early. Windows paths are case-insensitive, so normalise
75
+ // the drive letter to upper case and use backslashes consistently to match the
76
+ // style the file-observation keys already use.
77
+ function normalizeCwd(cwd) {
78
+ if (!cwd || typeof cwd !== 'string') return cwd
79
+ let out = cwd.replace(/\//g, '\\').replace(/\\+/g, '\\')
80
+ // Upper-case a single drive letter prefix ("c:\..." -> "C:\...").
81
+ if (/^[a-zA-Z]:\\/.test(out)) out = out[0].toUpperCase() + out.slice(1)
82
+ // Drop a trailing separator unless the path is just the drive root.
83
+ if (out.length > 3 && out.endsWith('\\')) out = out.slice(0, -1)
84
+ return out
85
+ }
86
+
87
+ // Parse the "YYYY/M/D HH:MM:SS | ..." stamp that context lines carry.
88
+ // Returns null when the line has no recognisable leading timestamp, so
89
+ // unparseable entries never get treated as the oldest by accident.
90
+ function contextTimestamp(line) {
91
+ const m = /^\s*(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})/.exec(String(line || ''))
92
+ if (!m) return null
93
+ const ms = new Date(
94
+ Number(m[1]),
95
+ Number(m[2]) - 1,
96
+ Number(m[3]),
97
+ Number(m[4]),
98
+ Number(m[5]),
99
+ Number(m[6]),
100
+ ).getTime()
101
+ return Number.isFinite(ms) ? ms : null
102
+ }
103
+
68
104
  // Drop repeated lines from stored context lists, preserving the most recent
69
105
  // occurrence order. Older builds could write the same line into every slot of
70
106
  // a workspace block (a generated digest headline broadcast to all projects),
71
107
  // so existing observe-state files still carry duplicates until they are
72
108
  // rewritten. Normalising on load heals that data without a migration step.
109
+ //
110
+ // Keys are also folded through normalizeCwd and merged, so buckets split by
111
+ // spelling differences collapse back into one on the next load.
73
112
  function dedupeContexts(contexts) {
74
113
  const out = {}
75
114
  for (const [cwd, list] of Object.entries(contexts)) {
115
+ const key = normalizeCwd(cwd)
116
+ const existing = out[key] || []
76
117
  if (!Array.isArray(list)) {
77
- out[cwd] = []
118
+ out[key] = existing
78
119
  continue
79
120
  }
80
- const seen = new Set()
81
- const deduped = []
121
+ const seen = new Set(existing)
122
+ const merged = existing.slice()
82
123
  for (const line of list) {
83
124
  if (seen.has(line)) continue
84
125
  seen.add(line)
85
- deduped.push(line)
126
+ merged.push(line)
86
127
  }
87
- out[cwd] = deduped
128
+ out[key] = merged
88
129
  }
89
130
  return out
90
131
  }
@@ -314,24 +355,39 @@ const CONTEXT_MAX = 6
314
355
  // retrieval guaranteed (not dependent on the model deciding to read memory).
315
356
  function updateProjectContext(state, cwd, line, targetFile) {
316
357
  if (!cwd || !line) return
358
+ const key = normalizeCwd(cwd)
317
359
  const target = path.join(cwd, targetFile || 'AGENTS.md')
318
- // Normalise the whole list before inserting, not just the incoming line.
319
- // Callers may load state without going through loadState (the digest
320
- // write-back reads the file directly), so stale duplicates have to be
321
- // cleared here or they keep consuming slots and push real history out.
360
+ // Merge any buckets that differ only by spelling, so a project observed as
361
+ // both "D:\work\code\x" and "d:\work\code\x" converges into one list instead
362
+ // of two half-filled ones.
322
363
  const list = []
323
364
  const seen = new Set()
324
- for (const item of state.contexts[cwd] || []) {
325
- if (seen.has(item)) continue
326
- seen.add(item)
327
- list.push(item)
365
+ for (const [existingKey, items] of Object.entries(state.contexts || {})) {
366
+ if (normalizeCwd(existingKey) !== key) continue
367
+ for (const item of items || []) {
368
+ if (seen.has(item)) continue
369
+ seen.add(item)
370
+ list.push(item)
371
+ }
372
+ if (existingKey !== key) delete state.contexts[existingKey]
328
373
  }
329
374
  // Re-inserting an existing line moves it to the front instead of duplicating.
330
375
  const existing = list.indexOf(line)
331
376
  if (existing !== -1) list.splice(existing, 1)
332
377
  list.unshift(line)
333
- if (list.length > CONTEXT_MAX) list.pop()
334
- state.contexts[cwd] = list
378
+ // Once buckets merge, the list can exceed CONTEXT_MAX. Entries are stamped
379
+ // "YYYY/M/D HH:MM:SS | ...", so trim by that timestamp rather than by
380
+ // position -- merging appends whole buckets in object-key order, which says
381
+ // nothing about age, and popping blind could discard recent work.
382
+ if (list.length > CONTEXT_MAX) {
383
+ const stamped = list.filter((l) => contextTimestamp(l) !== null)
384
+ if (stamped.length > 1) {
385
+ list.sort((a, b) => contextTimestamp(b) - contextTimestamp(a))
386
+ }
387
+ list.length = CONTEXT_MAX
388
+ }
389
+ state.contexts = state.contexts || {}
390
+ state.contexts[key] = list
335
391
  try {
336
392
  let content = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
337
393
  const block = CONTEXT_BEGIN + '\n## Mio 最近上下文(自动注入)\n' + list.map(function (l) { return '- ' + l }).join('\n') + '\n' + CONTEXT_END
@@ -1629,6 +1685,8 @@ module.exports = {
1629
1685
  loadState,
1630
1686
  saveState,
1631
1687
  dedupeContexts,
1688
+ normalizeCwd,
1689
+ contextTimestamp,
1632
1690
  updateProjectContext,
1633
1691
  runCycle,
1634
1692
  opencodeDbPath,
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "mio-agent-runtime",
3
- "version": "0.5.23",
4
- "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
- "license": "MIT",
6
- "type": "commonjs",
7
- "bin": {
8
- "mio": "bin/mio.js"
9
- },
10
- "files": [
11
- "bin/",
12
- "server/",
13
- "adapters/",
14
- "observe/",
15
- "scripts/",
16
- "README.md",
17
- "!server/mio-intelligence-mcp/__tests__"
18
- ],
19
- "scripts": {
20
- "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check adapters/claude-code.js && node --check server/runtime-modules.js && node --check server/memory-store.js && node --check server/retention.js && node --check server/digest.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
21
- "test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
22
- "prepack": "npm run check",
23
- "verify:pack-install": "node scripts/verify-packed-runtime.js"
24
- },
25
- "engines": {
26
- "node": ">=18"
27
- },
28
- "dependencies": {
29
- "@akemi-mio/runtime-contracts": "^0.1.0",
30
- "@akemi-mio/runtime-foundation": "^0.1.0",
31
- "@akemi-mio/experience-memory": "^0.1.0",
32
- "@akemi-mio/evolution-learning": "^0.1.0",
33
- "@akemi-mio/evolution-strategy": "^0.1.0",
34
- "@akemi-mio/evolution-safety": "^0.1.0",
35
- "@akemi-mio/evolution-scheduler": "^0.1.0",
36
- "@akemi-mio/observer": "^0.1.0",
37
- "@akemi-mio/insight": "^0.1.0"
38
- },
39
- "main": "index.js",
40
- "directories": {
41
- "doc": "docs"
42
- },
43
- "keywords": [],
44
- "author": ""
45
- }
1
+ {
2
+ "name": "mio-agent-runtime",
3
+ "version": "0.7.0",
4
+ "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "mio": "bin/mio.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "server/",
13
+ "adapters/",
14
+ "observe/",
15
+ "scripts/",
16
+ "README.md",
17
+ "!server/mio-intelligence-mcp/__tests__"
18
+ ],
19
+ "scripts": {
20
+ "check": "node scripts/check-syntax.cjs",
21
+ "test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
22
+ "prepack": "npm run check",
23
+ "verify:pack-install": "node scripts/verify-packed-runtime.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "@akemi-mio/runtime-contracts": "^0.1.0",
30
+ "@akemi-mio/runtime-foundation": "^0.1.0",
31
+ "@akemi-mio/experience-memory": "^0.1.0",
32
+ "@akemi-mio/evolution-learning": "^0.1.0",
33
+ "@akemi-mio/evolution-strategy": "^0.1.0",
34
+ "@akemi-mio/evolution-safety": "^0.1.0",
35
+ "@akemi-mio/evolution-scheduler": "^0.1.0",
36
+ "@akemi-mio/observer": "^0.1.0",
37
+ "@akemi-mio/insight": "^0.1.0"
38
+ },
39
+ "main": "index.js",
40
+ "directories": {
41
+ "doc": "docs"
42
+ },
43
+ "keywords": [],
44
+ "author": ""
45
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // Syntax gate for mio-agent-runtime (`npm run check`, which prepack runs).
5
+ //
6
+ // This used to be a hand-written file list in package.json. That silently rotted:
7
+ // nine server modules -- including every store added later (policy, agent, task,
8
+ // query-log, llm-client, creativity-engine) -- were never syntax-checked, so a
9
+ // typo in any of them would have survived `npm run check` and only exploded at
10
+ // require time, i.e. inside the published package.
11
+ //
12
+ // So: discover files instead of listing them. New modules are covered
13
+ // automatically, and the gate can no longer fall behind the codebase.
14
+
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ const { spawnSync } = require('child_process')
18
+
19
+ const ROOT = path.resolve(__dirname, '..')
20
+
21
+ // Top-level dirs to walk. __tests__ is excluded: those are exercised by
22
+ // `node --test`, and a broken test file should fail the test run, not the
23
+ // package gate.
24
+ const INCLUDE_DIRS = ['bin', 'adapters', 'observe', 'server', 'scripts']
25
+ const EXCLUDE_DIRS = new Set(['node_modules', '__tests__', 'dist', 'out'])
26
+ const EXTENSIONS = new Set(['.js', '.cjs'])
27
+
28
+ function walk(dir, out = []) {
29
+ let entries
30
+ try {
31
+ entries = fs.readdirSync(dir, { withFileTypes: true })
32
+ } catch (_) {
33
+ return out
34
+ }
35
+ for (const entry of entries) {
36
+ if (entry.isDirectory()) {
37
+ if (EXCLUDE_DIRS.has(entry.name)) continue
38
+ walk(path.join(dir, entry.name), out)
39
+ } else if (entry.isFile() && EXTENSIONS.has(path.extname(entry.name))) {
40
+ out.push(path.join(dir, entry.name))
41
+ }
42
+ }
43
+ return out
44
+ }
45
+
46
+ const files = INCLUDE_DIRS.flatMap((dir) => walk(path.join(ROOT, dir))).sort()
47
+
48
+ if (files.length === 0) {
49
+ console.error('check-syntax: no files discovered -- the gate is broken')
50
+ process.exit(1)
51
+ }
52
+
53
+ const failures = []
54
+ for (const file of files) {
55
+ const result = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
56
+ if (result.status !== 0) {
57
+ failures.push({ file, message: (result.stderr || '').trim().split('\n')[0] })
58
+ }
59
+ }
60
+
61
+ const rel = (f) => path.relative(ROOT, f).replace(/\\/g, '/')
62
+ console.log(`checked ${files.length} file(s)`)
63
+
64
+ if (failures.length > 0) {
65
+ for (const failure of failures) {
66
+ console.error(`FAIL ${rel(failure.file)}: ${failure.message}`)
67
+ }
68
+ console.error(`\n${failures.length} file(s) failed syntax check`)
69
+ process.exit(1)
70
+ }
71
+
72
+ console.log('all files parse')
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ // Shared agent store: the single implementation behind mio.agent.list /
4
+ // register / report, consumed by both the MCP server and the CLI
5
+ // (`mio agents list` / `mio agents report`). It owns agents.jsonl plus the
6
+ // read-only cross-references the report needs (traces, memory, experience
7
+ // reuse). Extracted from mio-intelligence-mcp/index.js so the terminal and the
8
+ // MCP tool can never report different agent telemetry.
9
+ //
10
+ // Two distinct "agent" notions live in this repo and the CLI must not confuse
11
+ // them: `mio agents` (no subcommand) lists installed host adapters from
12
+ // config.json, while this store reports *observed* agents recorded in
13
+ // agents.jsonl. `mio agents list` / `report` are about the latter.
14
+
15
+ const path = require('path')
16
+ const { createId, readJsonl, appendJsonl, writeJsonl } = require('./memory-store.js')
17
+
18
+ function createAgentStore(options) {
19
+ const dataDir = options.dataDir
20
+ const projectName = options.projectName || (() => null)
21
+
22
+ const agentsPath = path.join(dataDir, 'agents.jsonl')
23
+ const tracePath = path.join(dataDir, 'traces.jsonl')
24
+ const memoryPath = path.join(dataDir, 'memory.jsonl')
25
+ const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
26
+
27
+ function listAgents(args = {}) {
28
+ const project = args.project || null
29
+ let agents = readJsonl(agentsPath)
30
+ if (project) agents = agents.filter((a) => a.project === project)
31
+ return {
32
+ count: agents.length,
33
+ agents: agents.map((a) => ({
34
+ id: a.id,
35
+ agentId: a.agentId,
36
+ hostType: a.hostType,
37
+ capabilities: a.capabilities,
38
+ registeredAt: a.registeredAt,
39
+ lastSeenAt: a.lastSeenAt,
40
+ sessionCount: a.sessionCount,
41
+ taskCount: a.taskCount || 0,
42
+ successCount: a.successCount || 0,
43
+ failureCount: a.failureCount || 0,
44
+ project: a.project,
45
+ })),
46
+ }
47
+ }
48
+
49
+ function reportAgent(args = {}) {
50
+ const targetAgent = args.agentId || null
51
+ const project = args.project || projectName()
52
+ const agents = readJsonl(agentsPath)
53
+ const filtered = agents.filter(
54
+ (a) => (!targetAgent || a.agentId === targetAgent) && (!project || a.project === project)
55
+ )
56
+ const traces = readJsonl(tracePath)
57
+ const memories = readJsonl(memoryPath)
58
+ const reuses = readJsonl(experienceReusePath)
59
+ const reports = filtered.map((agent) => {
60
+ const agentTraces = traces.filter((t) => t.agent === agent.agentId)
61
+ const outcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
62
+ const successes = outcomes.filter((t) => t.outcome === 'success').length
63
+ const failures = outcomes.filter((t) => t.outcome === 'failure').length
64
+ const agentMemories = memories.filter((m) => m.source === agent.agentId)
65
+ const agentReuses = reuses.filter(
66
+ (r) => r.sourceAgent === agent.agentId || r.targetAgent === agent.agentId
67
+ )
68
+ const verified = agentReuses.filter(
69
+ (r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
70
+ )
71
+ const regTasks = agent.taskCount || 0
72
+ const regSuccess = agent.successCount || 0
73
+ const regFail = agent.failureCount || 0
74
+ const taskTotal = regTasks > 0 ? regTasks : outcomes.length
75
+ const taskSuccess = regTasks > 0 ? regSuccess : successes
76
+ const taskFail = regTasks > 0 ? regFail : failures
77
+ return {
78
+ agentId: agent.agentId,
79
+ hostType: agent.hostType,
80
+ sessionCount: agent.sessionCount,
81
+ registeredAt: agent.registeredAt,
82
+ lastSeenAt: agent.lastSeenAt,
83
+ taskOutcomes: {
84
+ total: taskTotal,
85
+ success: taskSuccess,
86
+ failure: taskFail,
87
+ successRate: taskTotal > 0 ? Math.round((taskSuccess / taskTotal) * 100) : 0,
88
+ },
89
+ memories: agentMemories.length,
90
+ experienceReuses: { total: agentReuses.length, verified: verified.length },
91
+ active: Date.now() - new Date(agent.lastSeenAt).getTime() < 24 * 60 * 60 * 1000,
92
+ }
93
+ })
94
+ return { project, count: reports.length, reports }
95
+ }
96
+
97
+ function registerAgent(args = {}) {
98
+ const agentId = String(args.agentId || '').trim()
99
+ if (!agentId) throw new Error('agent.register requires a non-empty agentId')
100
+ const project = args.project || projectName()
101
+ const hostType = args.hostType || 'mcp'
102
+ const capabilities = Array.isArray(args.capabilities) ? args.capabilities : []
103
+ const now = new Date().toISOString()
104
+ const agents = readJsonl(agentsPath)
105
+ const existing = agents.find((a) => a.agentId === agentId && a.project === project)
106
+ let result
107
+ if (existing) {
108
+ existing.lastSeenAt = now
109
+ existing.sessionCount = (existing.sessionCount || 1) + 1
110
+ existing.hostType = hostType
111
+ if (capabilities.length > 0) existing.capabilities = capabilities
112
+ result = existing
113
+ } else {
114
+ result = {
115
+ id: createId('agent'),
116
+ agentId,
117
+ hostType,
118
+ capabilities,
119
+ registeredAt: now,
120
+ lastSeenAt: now,
121
+ sessionCount: 1,
122
+ project,
123
+ }
124
+ agents.push(result)
125
+ }
126
+ writeJsonl(agentsPath, agents)
127
+ return {
128
+ id: result.id,
129
+ agentId: result.agentId,
130
+ registered: !existing,
131
+ sessionCount: result.sessionCount,
132
+ hostType: result.hostType,
133
+ capabilities: result.capabilities,
134
+ }
135
+ }
136
+
137
+ return { listAgents, reportAgent, registerAgent }
138
+ }
139
+
140
+ module.exports = { createAgentStore }