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.
- package/README.md +524 -85
- package/bin/mio.js +2861 -758
- package/observe/observer.js +73 -15
- package/package.json +45 -45
- package/scripts/check-syntax.cjs +72 -0
- package/server/agent-store.js +140 -0
- package/server/evolution-report.js +311 -0
- package/server/experience-store.js +214 -0
- package/server/insight-store.js +118 -0
- package/server/llm-client.js +161 -0
- package/server/memory-store.js +505 -3
- package/server/mio-intelligence-mcp/index.js +129 -1521
- package/server/observer-store.js +228 -0
- package/server/policy-store.js +263 -0
- package/server/query-log.js +66 -0
- package/server/subscription-store.js +213 -0
- package/server/task-store.js +385 -0
package/observe/observer.js
CHANGED
|
@@ -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[
|
|
118
|
+
out[key] = existing
|
|
78
119
|
continue
|
|
79
120
|
}
|
|
80
|
-
const seen = new Set()
|
|
81
|
-
const
|
|
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
|
-
|
|
126
|
+
merged.push(line)
|
|
86
127
|
}
|
|
87
|
-
out[
|
|
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
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
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
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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.
|
|
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
|
|
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 }
|