@ai-devkit/agent-manager 0.19.0 → 0.20.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/dist/__tests__/adapters/CodexAdapter.test.js +344 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CopilotAdapter.test.js +127 -2
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +144 -2
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +54 -5
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +1 -0
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +14 -2
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +4 -2
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
- package/dist/adapters/CopilotAdapter.js +23 -12
- package/dist/adapters/CopilotAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +1 -0
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +36 -11
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/utils/process.d.ts +11 -2
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -9
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/CodexAdapter.test.ts +289 -0
- package/src/__tests__/adapters/CopilotAdapter.test.ts +97 -5
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +139 -4
- package/src/__tests__/utils/process.test.ts +31 -7
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/CodexAdapter.ts +17 -2
- package/src/adapters/CopilotAdapter.ts +25 -14
- package/src/adapters/GeminiCliAdapter.ts +42 -11
- package/src/utils/process.ts +64 -9
|
@@ -215,13 +215,14 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
215
215
|
|
|
216
216
|
const stat = safeStat(filePath);
|
|
217
217
|
if (!stat) continue;
|
|
218
|
+
const metaTimestampMs = this.parseMetaTimestampMs(parsed.payload?.timestamp);
|
|
218
219
|
|
|
219
220
|
return {
|
|
220
221
|
sessionId,
|
|
221
222
|
filePath,
|
|
222
223
|
projectDir: path.dirname(filePath),
|
|
223
|
-
birthtimeMs: stat.birthtimeMs,
|
|
224
|
-
resolvedCwd: parsed.payload
|
|
224
|
+
birthtimeMs: metaTimestampMs ?? stat.birthtimeMs,
|
|
225
|
+
resolvedCwd: parsed.payload?.cwd || '',
|
|
225
226
|
};
|
|
226
227
|
} catch {
|
|
227
228
|
continue;
|
|
@@ -290,6 +291,10 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
290
291
|
const parsed = JSON.parse(firstLine);
|
|
291
292
|
if (parsed.type === 'session_meta') {
|
|
292
293
|
file.resolvedCwd = parsed.payload?.cwd || '';
|
|
294
|
+
const metaTimestampMs = this.parseMetaTimestampMs(parsed.payload?.timestamp);
|
|
295
|
+
if (metaTimestampMs !== null) {
|
|
296
|
+
file.birthtimeMs = metaTimestampMs;
|
|
297
|
+
}
|
|
293
298
|
}
|
|
294
299
|
}
|
|
295
300
|
} catch {
|
|
@@ -468,6 +473,16 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
468
473
|
return Number.isNaN(timestamp.getTime()) ? null : timestamp;
|
|
469
474
|
}
|
|
470
475
|
|
|
476
|
+
private parseMetaTimestampMs(value?: string): number | null {
|
|
477
|
+
if (typeof value !== 'string') return null;
|
|
478
|
+
|
|
479
|
+
const timestamp = this.parseTimestamp(value);
|
|
480
|
+
if (!timestamp) return null;
|
|
481
|
+
|
|
482
|
+
const timestampMs = timestamp.getTime();
|
|
483
|
+
return Number.isFinite(timestampMs) ? timestampMs : null;
|
|
484
|
+
}
|
|
485
|
+
|
|
471
486
|
private determineStatus(session: CodexSession): AgentStatus {
|
|
472
487
|
const diffMs = Date.now() - session.lastActive.getTime();
|
|
473
488
|
const diffMinutes = diffMs / 60000;
|
|
@@ -19,9 +19,10 @@ import type {
|
|
|
19
19
|
SessionSummary,
|
|
20
20
|
} from './AgentAdapter.js';
|
|
21
21
|
import { AgentStatus } from './AgentAdapter.js';
|
|
22
|
-
import { enrichProcesses, listAgentProcesses } from '../utils/process.js';
|
|
22
|
+
import { enrichProcesses, findWrapperProcess, findWrapperProcessPids, listAgentProcesses } from '../utils/process.js';
|
|
23
23
|
import { generateAgentName } from '../utils/matching.js';
|
|
24
24
|
import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
|
|
25
|
+
import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
|
|
25
26
|
|
|
26
27
|
interface CopilotEventEntry {
|
|
27
28
|
type?: string;
|
|
@@ -107,10 +108,12 @@ export class CopilotAdapter implements AgentAdapter {
|
|
|
107
108
|
]);
|
|
108
109
|
|
|
109
110
|
private sessionStateDir: string;
|
|
111
|
+
private registry: AgentRegistry;
|
|
110
112
|
|
|
111
|
-
constructor() {
|
|
113
|
+
constructor(registry: AgentRegistry = AgentRegistry.default()) {
|
|
112
114
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
113
115
|
this.sessionStateDir = path.join(homeDir, '.copilot', 'session-state');
|
|
116
|
+
this.registry = registry;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
canHandle(processInfo: ProcessInfo): boolean {
|
|
@@ -122,6 +125,7 @@ export class CopilotAdapter implements AgentAdapter {
|
|
|
122
125
|
if (processes.length === 0) return [];
|
|
123
126
|
|
|
124
127
|
const processByPid = new Map(processes.map((proc) => [proc.pid, proc]));
|
|
128
|
+
const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
|
|
125
129
|
const matchedPids = new Set<number>();
|
|
126
130
|
const matchedProcesses: ProcessInfo[] = [];
|
|
127
131
|
const agents: AgentInfo[] = [];
|
|
@@ -133,29 +137,36 @@ export class CopilotAdapter implements AgentAdapter {
|
|
|
133
137
|
const session = this.readSessionDir(lock.sessionDir, lock.sessionId);
|
|
134
138
|
if (!session) continue;
|
|
135
139
|
|
|
136
|
-
|
|
140
|
+
const agent = this.mapSessionToAgent(session, proc);
|
|
141
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
142
|
+
agents.push(agent);
|
|
137
143
|
matchedPids.add(proc.pid);
|
|
138
144
|
matchedProcesses.push(proc);
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
const wrapperPids = findWrapperProcessPids(processes, matchedProcesses);
|
|
141
148
|
for (const proc of processes) {
|
|
142
|
-
if (!matchedPids.has(proc.pid) && !
|
|
143
|
-
|
|
149
|
+
if (!matchedPids.has(proc.pid) && !wrapperPids.has(proc.pid)) {
|
|
150
|
+
const agent = this.mapProcessOnlyAgent(proc);
|
|
151
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
152
|
+
agents.push(agent);
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
return agents;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
|
-
private
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
private applyWrapperRegistryName(
|
|
160
|
+
agent: AgentInfo,
|
|
161
|
+
processInfo: ProcessInfo,
|
|
162
|
+
processes: ProcessInfo[],
|
|
163
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
164
|
+
): void {
|
|
165
|
+
const wrapper = findWrapperProcess(processes, processInfo);
|
|
166
|
+
const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
|
|
167
|
+
if (wrapperEntry?.type === this.type) {
|
|
168
|
+
agent.name = wrapperEntry.name;
|
|
169
|
+
}
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
@@ -22,11 +22,11 @@ import type {
|
|
|
22
22
|
ListSessionsOptions,
|
|
23
23
|
} from './AgentAdapter.js';
|
|
24
24
|
import { AgentStatus } from './AgentAdapter.js';
|
|
25
|
-
import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
|
|
25
|
+
import { listAgentProcesses, enrichProcesses, findWrapperProcess, findWrapperProcessPids } from '../utils/process.js';
|
|
26
26
|
import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
|
|
27
27
|
import type { SessionFile } from '../utils/session.js';
|
|
28
28
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
|
|
29
|
-
import { AgentRegistry } from '../utils/AgentRegistry.js';
|
|
29
|
+
import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* A single Gemini CLI message content part. Mirrors the `{text?: string}`
|
|
@@ -117,40 +117,70 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
117
117
|
const processes = nodeProcesses.filter((proc) => this.isGeminiExecutable(proc.command));
|
|
118
118
|
if (processes.length === 0) return [];
|
|
119
119
|
|
|
120
|
-
const
|
|
120
|
+
const wrapperPids = findWrapperProcessPids(processes);
|
|
121
|
+
const { cachedAgents, remaining } = this.tryRegistryCache(processes, wrapperPids);
|
|
121
122
|
if (remaining.length === 0) return this.deduplicateSessionAgents(cachedAgents);
|
|
122
123
|
|
|
123
|
-
const
|
|
124
|
+
const candidateProcesses = remaining.filter((proc) => !wrapperPids.has(proc.pid));
|
|
125
|
+
const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
|
|
126
|
+
|
|
127
|
+
const { sessions, contentCache } = this.discoverSessions(candidateProcesses);
|
|
124
128
|
if (sessions.length === 0) {
|
|
129
|
+
const processOnlyAgents = candidateProcesses.map((proc) => {
|
|
130
|
+
const agent = this.mapProcessOnlyAgent(proc);
|
|
131
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
132
|
+
return agent;
|
|
133
|
+
});
|
|
134
|
+
|
|
125
135
|
return this.deduplicateSessionAgents([
|
|
126
136
|
...cachedAgents,
|
|
127
|
-
...
|
|
137
|
+
...processOnlyAgents,
|
|
128
138
|
]);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
const matches = matchProcessesToSessions(
|
|
141
|
+
const matches = matchProcessesToSessions(candidateProcesses, sessions);
|
|
132
142
|
const matchedPids = new Set(matches.map((m) => m.process.pid));
|
|
143
|
+
const matchedProcesses: ProcessInfo[] = [];
|
|
133
144
|
const agents: AgentInfo[] = [];
|
|
134
145
|
|
|
135
146
|
for (const match of matches) {
|
|
136
147
|
const cachedContent = contentCache.get(match.session.filePath);
|
|
137
148
|
const sessionData = this.parseSession(cachedContent, match.session.filePath);
|
|
138
149
|
if (sessionData) {
|
|
139
|
-
|
|
150
|
+
const agent = this.mapSessionToAgent(sessionData, match.process, match.session.filePath);
|
|
151
|
+
this.applyWrapperRegistryName(agent, match.process, processes, registryEntriesByPid);
|
|
152
|
+
agents.push(agent);
|
|
153
|
+
matchedProcesses.push(match.process);
|
|
140
154
|
} else {
|
|
141
155
|
matchedPids.delete(match.process.pid);
|
|
142
156
|
}
|
|
143
157
|
}
|
|
144
158
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
const matchedWrapperPids = findWrapperProcessPids(candidateProcesses, matchedProcesses);
|
|
160
|
+
for (const proc of candidateProcesses) {
|
|
161
|
+
if (!matchedPids.has(proc.pid) && !matchedWrapperPids.has(proc.pid)) {
|
|
162
|
+
const agent = this.mapProcessOnlyAgent(proc);
|
|
163
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
164
|
+
agents.push(agent);
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
return this.deduplicateSessionAgents([...cachedAgents, ...agents]);
|
|
152
169
|
}
|
|
153
170
|
|
|
171
|
+
private applyWrapperRegistryName(
|
|
172
|
+
agent: AgentInfo,
|
|
173
|
+
processInfo: ProcessInfo,
|
|
174
|
+
processes: ProcessInfo[],
|
|
175
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
176
|
+
): void {
|
|
177
|
+
const wrapper = findWrapperProcess(processes, processInfo);
|
|
178
|
+
const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
|
|
179
|
+
if (wrapperEntry?.type === this.type) {
|
|
180
|
+
agent.name = wrapperEntry.name;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
private deduplicateSessionAgents(agents: AgentInfo[]): AgentInfo[] {
|
|
155
185
|
const bySession = new Map<string, AgentInfo>();
|
|
156
186
|
const result: AgentInfo[] = [];
|
|
@@ -187,7 +217,7 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
187
217
|
return null;
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
private tryRegistryCache(processes: ProcessInfo[]): {
|
|
220
|
+
private tryRegistryCache(processes: ProcessInfo[], wrapperPids: Set<number>): {
|
|
191
221
|
cachedAgents: AgentInfo[];
|
|
192
222
|
remaining: ProcessInfo[];
|
|
193
223
|
} {
|
|
@@ -198,6 +228,7 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
198
228
|
for (const proc of processes) {
|
|
199
229
|
const entry = byPid.get(proc.pid);
|
|
200
230
|
if (
|
|
231
|
+
wrapperPids.has(proc.pid) ||
|
|
201
232
|
!entry ||
|
|
202
233
|
entry.type !== this.type ||
|
|
203
234
|
!entry.sessionFilePath ||
|
package/src/utils/process.ts
CHANGED
|
@@ -12,10 +12,10 @@ import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* List running processes matching an agent executable name.
|
|
14
14
|
*
|
|
15
|
-
* Uses `ps
|
|
15
|
+
* Uses `ps -axo` then filters in JS for exact executable basename match.
|
|
16
16
|
* This avoids shell pipelines and string interpolation.
|
|
17
17
|
*
|
|
18
|
-
* Returned ProcessInfo has pid, command, tty populated.
|
|
18
|
+
* Returned ProcessInfo has pid, ppid, command, tty populated.
|
|
19
19
|
* cwd and startTime are NOT populated — call enrichProcesses() to fill them.
|
|
20
20
|
*/
|
|
21
21
|
export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
@@ -25,7 +25,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
|
-
const output = execFileSync('ps', ['
|
|
28
|
+
const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });
|
|
29
29
|
|
|
30
30
|
const lowerPattern = namePattern.toLowerCase();
|
|
31
31
|
const processes: ProcessInfo[] = [];
|
|
@@ -33,14 +33,15 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
33
33
|
for (const line of output.trim().split('\n')) {
|
|
34
34
|
if (!line.trim()) continue;
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
36
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
37
|
+
if (!match) continue;
|
|
38
38
|
|
|
39
|
-
const pid = parseInt(
|
|
40
|
-
|
|
39
|
+
const pid = parseInt(match[1], 10);
|
|
40
|
+
const ppid = parseInt(match[2], 10);
|
|
41
|
+
if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
|
|
41
42
|
|
|
42
|
-
const tty =
|
|
43
|
-
const command =
|
|
43
|
+
const tty = match[3];
|
|
44
|
+
const command = match[4];
|
|
44
45
|
|
|
45
46
|
// Check that the executable basename matches exactly
|
|
46
47
|
const executable = command.trim().split(/\s+/)[0] || '';
|
|
@@ -53,6 +54,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
53
54
|
|
|
54
55
|
processes.push({
|
|
55
56
|
pid,
|
|
57
|
+
ppid,
|
|
56
58
|
command,
|
|
57
59
|
cwd: '',
|
|
58
60
|
tty: ttyShort,
|
|
@@ -177,6 +179,59 @@ export function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {
|
|
|
177
179
|
return processes;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
function isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {
|
|
183
|
+
const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;
|
|
184
|
+
const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;
|
|
185
|
+
|
|
186
|
+
return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {
|
|
190
|
+
return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function findWrapperProcess(
|
|
194
|
+
processes: ProcessInfo[],
|
|
195
|
+
child: ProcessInfo,
|
|
196
|
+
): ProcessInfo | undefined {
|
|
197
|
+
return processes.find((proc) => (
|
|
198
|
+
proc.pid !== child.pid &&
|
|
199
|
+
child.ppid === proc.pid &&
|
|
200
|
+
matchesProcessIdentity(proc, child)
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Find parent wrapper processes that should not be reported as separate agents.
|
|
206
|
+
*
|
|
207
|
+
* A process is considered a wrapper when it is the parent of another candidate
|
|
208
|
+
* agent process in the same terminal/worktree, or when it points at the same
|
|
209
|
+
* terminal/worktree as an already session-matched process.
|
|
210
|
+
*/
|
|
211
|
+
export function findWrapperProcessPids(
|
|
212
|
+
processes: ProcessInfo[],
|
|
213
|
+
matchedProcesses: ProcessInfo[] = [],
|
|
214
|
+
): Set<number> {
|
|
215
|
+
const wrappers = new Set<number>();
|
|
216
|
+
|
|
217
|
+
for (const child of processes) {
|
|
218
|
+
const wrapper = findWrapperProcess(processes, child);
|
|
219
|
+
if (wrapper) {
|
|
220
|
+
wrappers.add(wrapper.pid);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const proc of processes) {
|
|
225
|
+
if (matchedProcesses.some((matched) => (
|
|
226
|
+
proc.pid !== matched.pid && isSameTerminalProcess(proc, matched)
|
|
227
|
+
))) {
|
|
228
|
+
wrappers.add(proc.pid);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return wrappers;
|
|
233
|
+
}
|
|
234
|
+
|
|
180
235
|
/**
|
|
181
236
|
* Get the TTY device for a specific process
|
|
182
237
|
*/
|