@ai-devkit/agent-manager 0.19.0 → 0.19.1
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/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/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/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/CopilotAdapter.ts +25 -14
- package/src/adapters/GeminiCliAdapter.ts +42 -11
- package/src/utils/process.ts +64 -9
|
@@ -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
|
*/
|