@ai-devkit/agent-manager 0.26.0 → 0.26.2
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/dist/__tests__/AgentManager.test.js +37 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/adapters/CodexAdapter.test.js +249 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +68 -46
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +9 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +106 -24
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/database/connection.d.ts +22 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +58 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +4 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/migrations/001_initial.sql +12 -0
- package/dist/database/schema.d.ts +4 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +47 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/utils/AgentRegistry.d.ts +7 -3
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +108 -57
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/AgentManager.test.ts +37 -0
- package/src/__tests__/adapters/CodexAdapter.test.ts +155 -0
- package/src/__tests__/utils/AgentRegistry.test.ts +48 -40
- package/src/adapters/CodexAdapter.ts +147 -27
- package/src/database/connection.ts +74 -0
- package/src/database/index.ts +7 -0
- package/src/database/migrations/001_initial.sql +12 -0
- package/src/database/schema.ts +62 -0
- package/src/utils/AgentRegistry.ts +109 -49
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { DatabaseConnection, resolveAgentRegistryDbPath } from '../database/index.js';
|
|
4
4
|
export class RenameNotFoundError extends Error {
|
|
5
5
|
agentName;
|
|
6
6
|
constructor(agentName){
|
|
@@ -18,9 +18,11 @@ export class RenameConflictError extends Error {
|
|
|
18
18
|
const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');
|
|
19
19
|
let defaultInstance = null;
|
|
20
20
|
export class AgentRegistry {
|
|
21
|
-
|
|
21
|
+
db;
|
|
22
22
|
constructor(filePath = DEFAULT_REGISTRY_PATH){
|
|
23
|
-
this.
|
|
23
|
+
this.db = new DatabaseConnection({
|
|
24
|
+
dbPath: resolveAgentRegistryDbPath(filePath)
|
|
25
|
+
});
|
|
24
26
|
}
|
|
25
27
|
static default() {
|
|
26
28
|
if (!defaultInstance) {
|
|
@@ -28,35 +30,80 @@ export class AgentRegistry {
|
|
|
28
30
|
}
|
|
29
31
|
return defaultInstance;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
writeFile(data) {
|
|
45
|
-
const dir = path.dirname(this.filePath);
|
|
46
|
-
fs.mkdirSync(dir, {
|
|
47
|
-
recursive: true
|
|
48
|
-
});
|
|
49
|
-
const tmp = `${this.filePath}.tmp`;
|
|
50
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
51
|
-
fs.renameSync(tmp, this.filePath);
|
|
33
|
+
rowToEntry(row) {
|
|
34
|
+
return {
|
|
35
|
+
name: row.name,
|
|
36
|
+
type: row.type,
|
|
37
|
+
pid: row.pid,
|
|
38
|
+
tmuxSession: row.tmux_session,
|
|
39
|
+
cwd: row.cwd,
|
|
40
|
+
startedAt: row.started_at,
|
|
41
|
+
sessionId: row.session_id,
|
|
42
|
+
sessionFilePath: row.session_file_path
|
|
43
|
+
};
|
|
52
44
|
}
|
|
53
45
|
mergeEntry(incoming, existing) {
|
|
54
46
|
if (!existing) return incoming;
|
|
47
|
+
const incomingIsManaged = Boolean(incoming.tmuxSession);
|
|
55
48
|
return {
|
|
56
|
-
...
|
|
57
|
-
|
|
49
|
+
...existing,
|
|
50
|
+
name: incomingIsManaged ? incoming.name : existing.name,
|
|
51
|
+
tmuxSession: incoming.tmuxSession || existing.tmuxSession,
|
|
52
|
+
cwd: incoming.cwd || existing.cwd,
|
|
53
|
+
startedAt: existing.startedAt || incoming.startedAt,
|
|
54
|
+
sessionId: incoming.sessionId || existing.sessionId,
|
|
55
|
+
sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath
|
|
58
56
|
};
|
|
59
57
|
}
|
|
58
|
+
findByIdentity(type, pid) {
|
|
59
|
+
const row = this.db.queryOne('SELECT * FROM agents WHERE type = ? AND pid = ?', [
|
|
60
|
+
type,
|
|
61
|
+
pid
|
|
62
|
+
]);
|
|
63
|
+
return row ? this.rowToEntry(row) : undefined;
|
|
64
|
+
}
|
|
65
|
+
findByName(name) {
|
|
66
|
+
const row = this.db.queryOne('SELECT * FROM agents WHERE name = ?', [
|
|
67
|
+
name
|
|
68
|
+
]);
|
|
69
|
+
return row ? this.rowToEntry(row) : undefined;
|
|
70
|
+
}
|
|
71
|
+
deleteNameConflict(name, type, pid) {
|
|
72
|
+
const conflict = this.findByName(name);
|
|
73
|
+
if (!conflict) return;
|
|
74
|
+
if (conflict.type === type && conflict.pid === pid) return;
|
|
75
|
+
if (!this.isAlive(conflict)) {
|
|
76
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
|
|
77
|
+
conflict.type,
|
|
78
|
+
conflict.pid
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
insertOrUpdate(entry) {
|
|
83
|
+
this.db.instance.prepare(`
|
|
84
|
+
INSERT INTO agents (
|
|
85
|
+
type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at
|
|
86
|
+
)
|
|
87
|
+
VALUES (
|
|
88
|
+
@type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt
|
|
89
|
+
)
|
|
90
|
+
ON CONFLICT(type, pid) DO UPDATE SET
|
|
91
|
+
name = excluded.name,
|
|
92
|
+
tmux_session = excluded.tmux_session,
|
|
93
|
+
cwd = excluded.cwd,
|
|
94
|
+
started_at = agents.started_at,
|
|
95
|
+
session_id = excluded.session_id,
|
|
96
|
+
session_file_path = excluded.session_file_path,
|
|
97
|
+
updated_at = excluded.updated_at
|
|
98
|
+
`).run({
|
|
99
|
+
...entry,
|
|
100
|
+
updatedAt: new Date().toISOString()
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
save(entry) {
|
|
104
|
+
this.deleteNameConflict(entry.name, entry.type, entry.pid);
|
|
105
|
+
this.insertOrUpdate(entry);
|
|
106
|
+
}
|
|
60
107
|
isAlive(entry) {
|
|
61
108
|
try {
|
|
62
109
|
process.kill(entry.pid, 0);
|
|
@@ -66,13 +113,16 @@ export class AgentRegistry {
|
|
|
66
113
|
}
|
|
67
114
|
}
|
|
68
115
|
prune() {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
116
|
+
const entries = this.list();
|
|
117
|
+
const stale = entries.filter((e)=>!this.isAlive(e));
|
|
118
|
+
this.db.transaction(()=>{
|
|
119
|
+
for (const entry of stale){
|
|
120
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
|
|
121
|
+
entry.type,
|
|
122
|
+
entry.pid
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
76
126
|
}
|
|
77
127
|
register(entry) {
|
|
78
128
|
this.registerBatch([
|
|
@@ -81,42 +131,43 @@ export class AgentRegistry {
|
|
|
81
131
|
}
|
|
82
132
|
registerBatch(entries) {
|
|
83
133
|
if (entries.length === 0) return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);
|
|
89
|
-
} else {
|
|
90
|
-
data.entries.push(incoming);
|
|
134
|
+
this.db.transaction(()=>{
|
|
135
|
+
for (const incoming of entries){
|
|
136
|
+
const existing = this.findByIdentity(incoming.type, incoming.pid);
|
|
137
|
+
this.save(this.mergeEntry(incoming, existing));
|
|
91
138
|
}
|
|
92
|
-
}
|
|
93
|
-
this.writeFile(data);
|
|
139
|
+
});
|
|
94
140
|
}
|
|
95
141
|
rename(currentName, newName) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (idx < 0) {
|
|
142
|
+
const existing = this.findByName(currentName);
|
|
143
|
+
if (!existing) {
|
|
99
144
|
throw new RenameNotFoundError(currentName);
|
|
100
145
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (conflict) {
|
|
146
|
+
const conflict = this.findByName(newName);
|
|
147
|
+
if (conflict && this.isAlive(conflict)) {
|
|
104
148
|
throw new RenameConflictError(newName);
|
|
105
149
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
150
|
+
this.db.transaction(()=>{
|
|
151
|
+
if (conflict) {
|
|
152
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
|
|
153
|
+
conflict.type,
|
|
154
|
+
conflict.pid
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
this.db.execute('UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?', [
|
|
158
|
+
newName,
|
|
159
|
+
new Date().toISOString(),
|
|
160
|
+
existing.type,
|
|
161
|
+
existing.pid
|
|
162
|
+
]);
|
|
112
163
|
});
|
|
113
164
|
}
|
|
114
165
|
lookup(name) {
|
|
115
|
-
|
|
116
|
-
return data.entries.find((e)=>e.name === name) ?? null;
|
|
166
|
+
return this.findByName(name) ?? null;
|
|
117
167
|
}
|
|
118
168
|
list() {
|
|
119
|
-
|
|
169
|
+
const rows = this.db.query('SELECT * FROM agents ORDER BY started_at ASC, name ASC');
|
|
170
|
+
return rows.map((row)=>this.rowToEntry(row));
|
|
120
171
|
}
|
|
121
172
|
}
|
|
122
173
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/AgentRegistry.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport class RenameNotFoundError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" not found in registry.`);\n this.name = 'RenameNotFoundError';\n }\n}\n\nexport class RenameConflictError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" is already in use.`);\n this.name = 'RenameConflictError';\n }\n}\n\nexport interface RegistryEntry {\n name: string;\n type: AgentType;\n pid: number;\n tmuxSession: string;\n cwd: string;\n startedAt: string; // ISO 8601\n sessionId: string;\n sessionFilePath: string;\n}\n\ninterface RegistryFile {\n entries: RegistryEntry[];\n}\n\nconst DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');\n\nlet defaultInstance: AgentRegistry | null = null;\n\nexport class AgentRegistry {\n private filePath: string;\n\n constructor(filePath: string = DEFAULT_REGISTRY_PATH) {\n this.filePath = filePath;\n }\n\n static default(): AgentRegistry {\n if (!defaultInstance) {\n defaultInstance = new AgentRegistry();\n }\n return defaultInstance;\n }\n\n private readFile(): RegistryFile {\n try {\n const raw = fs.readFileSync(this.filePath, 'utf8');\n const parsed = JSON.parse(raw) as RegistryFile;\n return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };\n } catch {\n return { entries: [] };\n }\n }\n\n private writeFile(data: RegistryFile): void {\n const dir = path.dirname(this.filePath);\n fs.mkdirSync(dir, { recursive: true });\n const tmp = `${this.filePath}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');\n fs.renameSync(tmp, this.filePath);\n }\n\n private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {\n if (!existing) return incoming;\n return {\n ...incoming,\n tmuxSession: incoming.tmuxSession || existing.tmuxSession,\n };\n }\n\n isAlive(entry: RegistryEntry): boolean {\n try {\n process.kill(entry.pid, 0);\n return true;\n } catch {\n return false;\n }\n }\n\n prune(): void {\n const data = this.readFile();\n const live = data.entries.filter((e) => this.isAlive(e));\n if (live.length !== data.entries.length) {\n this.writeFile({ entries: live });\n }\n }\n\n register(entry: RegistryEntry): void {\n this.registerBatch([entry]);\n }\n\n registerBatch(entries: RegistryEntry[]): void {\n if (entries.length === 0) return;\n const data = this.readFile();\n for (const incoming of entries) {\n const idx = data.entries.findIndex((e) => e.name === incoming.name);\n if (idx >= 0) {\n data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);\n } else {\n data.entries.push(incoming);\n }\n }\n this.writeFile(data);\n }\n\n rename(currentName: string, newName: string): void {\n const data = this.readFile();\n const idx = data.entries.findIndex((e) => e.name === currentName);\n if (idx < 0) {\n throw new RenameNotFoundError(currentName);\n }\n const liveEntries = data.entries.filter((e) => this.isAlive(e));\n const conflict = liveEntries.find((e) => e.name === newName);\n if (conflict) {\n throw new RenameConflictError(newName);\n }\n const pruned = liveEntries.map((e) =>\n e.name === currentName ? { ...e, name: newName } : e,\n );\n this.writeFile({ entries: pruned });\n }\n\n lookup(name: string): RegistryEntry | null {\n const data = this.readFile();\n return data.entries.find((e) => e.name === name) ?? null;\n }\n\n list(): RegistryEntry[] {\n return this.readFile().entries;\n }\n}\n"],"names":["fs","os","path","RenameNotFoundError","Error","agentName","name","RenameConflictError","DEFAULT_REGISTRY_PATH","join","homedir","defaultInstance","AgentRegistry","filePath","default","readFile","raw","readFileSync","parsed","JSON","parse","entries","Array","isArray","writeFile","data","dir","dirname","mkdirSync","recursive","tmp","writeFileSync","stringify","renameSync","mergeEntry","incoming","existing","tmuxSession","isAlive","entry","process","kill","pid","prune","live","filter","e","length","register","registerBatch","idx","findIndex","push","rename","currentName","newName","liveEntries","conflict","find","pruned","map","lookup","list"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/AgentRegistry.ts"],"sourcesContent":["import os from 'os';\nimport path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\nimport {\n DatabaseConnection,\n resolveAgentRegistryDbPath,\n} from '../database/index.js';\n\nexport class RenameNotFoundError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" not found in registry.`);\n this.name = 'RenameNotFoundError';\n }\n}\n\nexport class RenameConflictError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" is already in use.`);\n this.name = 'RenameConflictError';\n }\n}\n\nexport interface RegistryEntry {\n name: string;\n type: AgentType;\n pid: number;\n tmuxSession: string;\n cwd: string;\n startedAt: string; // ISO 8601\n sessionId: string;\n sessionFilePath: string;\n}\n\ninterface RegistryRow {\n name: string;\n type: AgentType;\n pid: number;\n tmux_session: string;\n cwd: string;\n started_at: string;\n session_id: string;\n session_file_path: string;\n updated_at: string;\n}\n\nconst DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');\n\nlet defaultInstance: AgentRegistry | null = null;\n\nexport class AgentRegistry {\n private db: DatabaseConnection;\n\n constructor(filePath: string = DEFAULT_REGISTRY_PATH) {\n this.db = new DatabaseConnection({ dbPath: resolveAgentRegistryDbPath(filePath) });\n }\n\n static default(): AgentRegistry {\n if (!defaultInstance) {\n defaultInstance = new AgentRegistry();\n }\n return defaultInstance;\n }\n\n private rowToEntry(row: RegistryRow): RegistryEntry {\n return {\n name: row.name,\n type: row.type,\n pid: row.pid,\n tmuxSession: row.tmux_session,\n cwd: row.cwd,\n startedAt: row.started_at,\n sessionId: row.session_id,\n sessionFilePath: row.session_file_path,\n };\n }\n\n private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {\n if (!existing) return incoming;\n const incomingIsManaged = Boolean(incoming.tmuxSession);\n return {\n ...existing,\n name: incomingIsManaged ? incoming.name : existing.name,\n tmuxSession: incoming.tmuxSession || existing.tmuxSession,\n cwd: incoming.cwd || existing.cwd,\n startedAt: existing.startedAt || incoming.startedAt,\n sessionId: incoming.sessionId || existing.sessionId,\n sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath,\n };\n }\n\n private findByIdentity(type: AgentType, pid: number): RegistryEntry | undefined {\n const row = this.db.queryOne<RegistryRow>(\n 'SELECT * FROM agents WHERE type = ? AND pid = ?',\n [type, pid],\n );\n return row ? this.rowToEntry(row) : undefined;\n }\n\n private findByName(name: string): RegistryEntry | undefined {\n const row = this.db.queryOne<RegistryRow>('SELECT * FROM agents WHERE name = ?', [name]);\n return row ? this.rowToEntry(row) : undefined;\n }\n\n private deleteNameConflict(name: string, type: AgentType, pid: number): void {\n const conflict = this.findByName(name);\n if (!conflict) return;\n if (conflict.type === type && conflict.pid === pid) return;\n if (!this.isAlive(conflict)) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);\n }\n }\n\n private insertOrUpdate(entry: RegistryEntry): void {\n this.db.instance.prepare(`\n INSERT INTO agents (\n type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at\n )\n VALUES (\n @type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt\n )\n ON CONFLICT(type, pid) DO UPDATE SET\n name = excluded.name,\n tmux_session = excluded.tmux_session,\n cwd = excluded.cwd,\n started_at = agents.started_at,\n session_id = excluded.session_id,\n session_file_path = excluded.session_file_path,\n updated_at = excluded.updated_at\n `).run({ ...entry, updatedAt: new Date().toISOString() });\n }\n\n private save(entry: RegistryEntry): void {\n this.deleteNameConflict(entry.name, entry.type, entry.pid);\n this.insertOrUpdate(entry);\n }\n\n isAlive(entry: RegistryEntry): boolean {\n try {\n process.kill(entry.pid, 0);\n return true;\n } catch {\n return false;\n }\n }\n\n prune(): void {\n const entries = this.list();\n const stale = entries.filter((e) => !this.isAlive(e));\n this.db.transaction(() => {\n for (const entry of stale) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [entry.type, entry.pid]);\n }\n });\n }\n\n register(entry: RegistryEntry): void {\n this.registerBatch([entry]);\n }\n\n registerBatch(entries: RegistryEntry[]): void {\n if (entries.length === 0) return;\n this.db.transaction(() => {\n for (const incoming of entries) {\n const existing = this.findByIdentity(incoming.type, incoming.pid);\n this.save(this.mergeEntry(incoming, existing));\n }\n });\n }\n\n rename(currentName: string, newName: string): void {\n const existing = this.findByName(currentName);\n if (!existing) {\n throw new RenameNotFoundError(currentName);\n }\n const conflict = this.findByName(newName);\n if (conflict && this.isAlive(conflict)) {\n throw new RenameConflictError(newName);\n }\n\n this.db.transaction(() => {\n if (conflict) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);\n }\n this.db.execute(\n 'UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?',\n [newName, new Date().toISOString(), existing.type, existing.pid],\n );\n });\n }\n\n lookup(name: string): RegistryEntry | null {\n return this.findByName(name) ?? null;\n }\n\n list(): RegistryEntry[] {\n const rows = this.db.query<RegistryRow>('SELECT * FROM agents ORDER BY started_at ASC, name ASC');\n return rows.map((row) => this.rowToEntry(row));\n }\n}\n"],"names":["os","path","DatabaseConnection","resolveAgentRegistryDbPath","RenameNotFoundError","Error","agentName","name","RenameConflictError","DEFAULT_REGISTRY_PATH","join","homedir","defaultInstance","AgentRegistry","db","filePath","dbPath","default","rowToEntry","row","type","pid","tmuxSession","tmux_session","cwd","startedAt","started_at","sessionId","session_id","sessionFilePath","session_file_path","mergeEntry","incoming","existing","incomingIsManaged","Boolean","findByIdentity","queryOne","undefined","findByName","deleteNameConflict","conflict","isAlive","execute","insertOrUpdate","entry","instance","prepare","run","updatedAt","Date","toISOString","save","process","kill","prune","entries","list","stale","filter","e","transaction","register","registerBatch","length","rename","currentName","newName","lookup","rows","query","map"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-devkit/agent-manager",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standalone agent detection and management utilities for AI DevKit",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
15
|
+
"build": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly && cp -r src/database/migrations dist/database/",
|
|
16
16
|
"dev": "swc src -d dist --strip-leading-paths --watch",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:watch": "vitest",
|
|
@@ -340,6 +340,43 @@ describe('AgentManager', () => {
|
|
|
340
340
|
expect(registry.list()[0].startedAt).toBe('2026-05-30T00:00:00.000Z');
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
it('preserves a user-managed name when a fallback row was written later for the same pid', async () => {
|
|
344
|
+
registry.register({
|
|
345
|
+
name: 'agent-list-debug',
|
|
346
|
+
type: 'codex',
|
|
347
|
+
pid: process.pid,
|
|
348
|
+
tmuxSession: 'agent-list-debug',
|
|
349
|
+
cwd: '/cwd/debug',
|
|
350
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
351
|
+
sessionId: 'pid-debug',
|
|
352
|
+
sessionFilePath: '',
|
|
353
|
+
});
|
|
354
|
+
registry.register({
|
|
355
|
+
name: `ai-devkit-${process.pid}`,
|
|
356
|
+
type: 'codex',
|
|
357
|
+
pid: process.pid,
|
|
358
|
+
tmuxSession: '',
|
|
359
|
+
cwd: '/cwd/debug',
|
|
360
|
+
startedAt: '2026-05-31T00:00:00.000Z',
|
|
361
|
+
sessionId: 'pid-debug',
|
|
362
|
+
sessionFilePath: '',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
scopedManager.registerAdapter(new MockAdapter('codex', [
|
|
366
|
+
createMockAgent({ name: `ai-devkit-${process.pid}`, type: 'codex', pid: process.pid }),
|
|
367
|
+
]));
|
|
368
|
+
|
|
369
|
+
const agents = await scopedManager.listAgents();
|
|
370
|
+
|
|
371
|
+
expect(agents[0].name).toBe('agent-list-debug');
|
|
372
|
+
expect(registry.list()).toHaveLength(1);
|
|
373
|
+
expect(registry.list()[0]).toMatchObject({
|
|
374
|
+
name: 'agent-list-debug',
|
|
375
|
+
pid: process.pid,
|
|
376
|
+
tmuxSession: 'agent-list-debug',
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
343
380
|
it('writes a fresh startedAt for new entries', async () => {
|
|
344
381
|
const before = new Date().toISOString();
|
|
345
382
|
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
@@ -1133,6 +1133,52 @@ describe('CodexAdapter', () => {
|
|
|
1133
1133
|
expect(session.summary).toBe('Last message');
|
|
1134
1134
|
});
|
|
1135
1135
|
|
|
1136
|
+
it('should extract summary from current Codex response_item messages', () => {
|
|
1137
|
+
const parseSession = (adapter as any).parseSession.bind(adapter);
|
|
1138
|
+
const filePath = path.join(tmpDir, 'response-item-summary.jsonl');
|
|
1139
|
+
fs.writeFileSync(filePath, [
|
|
1140
|
+
JSON.stringify({ type: 'session_meta', payload: { id: 'sess-ri', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }),
|
|
1141
|
+
JSON.stringify({
|
|
1142
|
+
type: 'response_item',
|
|
1143
|
+
timestamp: '2026-03-18T15:01:00Z',
|
|
1144
|
+
payload: {
|
|
1145
|
+
type: 'message',
|
|
1146
|
+
role: 'assistant',
|
|
1147
|
+
content: [{ type: 'output_text', text: 'Parsed from current schema' }],
|
|
1148
|
+
},
|
|
1149
|
+
}),
|
|
1150
|
+
].join('\n'));
|
|
1151
|
+
|
|
1152
|
+
const session = parseSession(undefined, filePath);
|
|
1153
|
+
expect(session.summary).toBe('Parsed from current schema');
|
|
1154
|
+
expect(session.lastPayloadType).toBe('agent_message');
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('should treat completed Codex AgentMessage events as waiting for list status', () => {
|
|
1158
|
+
const parseSession = (adapter as any).parseSession.bind(adapter);
|
|
1159
|
+
const determineStatus = (adapter as any).determineStatus.bind(adapter);
|
|
1160
|
+
const filePath = path.join(tmpDir, 'agent-message-status.jsonl');
|
|
1161
|
+
fs.writeFileSync(filePath, [
|
|
1162
|
+
JSON.stringify({ type: 'session_meta', payload: { id: 'sess-am', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }),
|
|
1163
|
+
JSON.stringify({
|
|
1164
|
+
type: 'event_msg',
|
|
1165
|
+
timestamp: new Date().toISOString(),
|
|
1166
|
+
payload: {
|
|
1167
|
+
type: 'item_completed',
|
|
1168
|
+
item: {
|
|
1169
|
+
type: 'AgentMessage',
|
|
1170
|
+
content: [{ type: 'Text', text: 'Waiting for the user now' }],
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
}),
|
|
1174
|
+
].join('\n'));
|
|
1175
|
+
|
|
1176
|
+
const session = parseSession(undefined, filePath);
|
|
1177
|
+
expect(session.summary).toBe('Waiting for the user now');
|
|
1178
|
+
expect(session.lastPayloadType).toBe('agent_message');
|
|
1179
|
+
expect(determineStatus(session)).toBe(AgentStatus.WAITING);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1136
1182
|
it('should handle malformed JSON lines gracefully', () => {
|
|
1137
1183
|
const parseSession = (adapter as any).parseSession.bind(adapter);
|
|
1138
1184
|
const filePath = path.join(tmpDir, 'malformed.jsonl');
|
|
@@ -1212,6 +1258,115 @@ describe('CodexAdapter', () => {
|
|
|
1212
1258
|
expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' });
|
|
1213
1259
|
});
|
|
1214
1260
|
|
|
1261
|
+
it('should parse Codex response_item message records', () => {
|
|
1262
|
+
const filePath = writeJsonl([
|
|
1263
|
+
{ type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
|
|
1264
|
+
{
|
|
1265
|
+
type: 'response_item',
|
|
1266
|
+
timestamp: '2026-03-27T10:00:01Z',
|
|
1267
|
+
payload: {
|
|
1268
|
+
type: 'message',
|
|
1269
|
+
role: 'user',
|
|
1270
|
+
content: [{ type: 'input_text', text: 'Fix the bug' }],
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
type: 'response_item',
|
|
1275
|
+
timestamp: '2026-03-27T10:00:05Z',
|
|
1276
|
+
payload: {
|
|
1277
|
+
type: 'message',
|
|
1278
|
+
role: 'assistant',
|
|
1279
|
+
content: [{ type: 'output_text', text: 'I found the issue' }],
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
]);
|
|
1283
|
+
|
|
1284
|
+
const messages = adapter.getConversation(filePath);
|
|
1285
|
+
expect(messages).toHaveLength(2);
|
|
1286
|
+
expect(messages[0]).toEqual({ role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' });
|
|
1287
|
+
expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' });
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
it('should parse completed Codex AgentMessage event records', () => {
|
|
1291
|
+
const filePath = writeJsonl([
|
|
1292
|
+
{ type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
|
|
1293
|
+
{
|
|
1294
|
+
type: 'event_msg',
|
|
1295
|
+
timestamp: '2026-03-27T10:00:05Z',
|
|
1296
|
+
payload: {
|
|
1297
|
+
type: 'item_completed',
|
|
1298
|
+
item: {
|
|
1299
|
+
type: 'AgentMessage',
|
|
1300
|
+
content: [{ type: 'Text', text: 'I found the issue' }],
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
]);
|
|
1305
|
+
|
|
1306
|
+
const messages = adapter.getConversation(filePath);
|
|
1307
|
+
expect(messages).toEqual([
|
|
1308
|
+
{ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' },
|
|
1309
|
+
]);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it('should not duplicate mirrored current Codex message records', () => {
|
|
1313
|
+
const filePath = writeJsonl([
|
|
1314
|
+
{ type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
|
|
1315
|
+
{
|
|
1316
|
+
type: 'response_item',
|
|
1317
|
+
timestamp: '2026-03-27T10:00:01Z',
|
|
1318
|
+
payload: {
|
|
1319
|
+
type: 'message',
|
|
1320
|
+
role: 'user',
|
|
1321
|
+
content: [{ type: 'input_text', text: 'Fix the bug' }],
|
|
1322
|
+
internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' },
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
type: 'event_msg',
|
|
1327
|
+
timestamp: '2026-03-27T10:00:01.001Z',
|
|
1328
|
+
payload: {
|
|
1329
|
+
type: 'item_completed',
|
|
1330
|
+
turn_id: 'turn-1',
|
|
1331
|
+
item: {
|
|
1332
|
+
type: 'UserMessage',
|
|
1333
|
+
content: [{ type: 'text', text: 'Fix the bug' }],
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
type: 'event_msg',
|
|
1339
|
+
timestamp: '2026-03-27T10:00:05Z',
|
|
1340
|
+
payload: {
|
|
1341
|
+
type: 'item_completed',
|
|
1342
|
+
turn_id: 'turn-1',
|
|
1343
|
+
item: {
|
|
1344
|
+
type: 'AgentMessage',
|
|
1345
|
+
id: 'msg-1',
|
|
1346
|
+
content: [{ type: 'Text', text: 'I found the issue' }],
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
type: 'response_item',
|
|
1352
|
+
timestamp: '2026-03-27T10:00:05.005Z',
|
|
1353
|
+
payload: {
|
|
1354
|
+
type: 'message',
|
|
1355
|
+
id: 'msg-1',
|
|
1356
|
+
role: 'assistant',
|
|
1357
|
+
content: [{ type: 'output_text', text: 'I found the issue' }],
|
|
1358
|
+
internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' },
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
]);
|
|
1362
|
+
|
|
1363
|
+
const messages = adapter.getConversation(filePath);
|
|
1364
|
+
expect(messages).toEqual([
|
|
1365
|
+
{ role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' },
|
|
1366
|
+
{ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05.005Z' },
|
|
1367
|
+
]);
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1215
1370
|
it('should skip session_meta entry', () => {
|
|
1216
1371
|
const filePath = writeJsonl([
|
|
1217
1372
|
{ type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
|