@ai-devkit/agent-manager 0.35.0 → 0.36.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.
Files changed (92) hide show
  1. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
  2. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
  3. package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
  4. package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
  5. package/dist/__tests__/capacity/claude.test.js +485 -0
  6. package/dist/__tests__/capacity/claude.test.js.map +1 -0
  7. package/dist/__tests__/capacity/openai.test.js +672 -0
  8. package/dist/__tests__/capacity/openai.test.js.map +1 -0
  9. package/dist/__tests__/capacity/wham.test.js +144 -0
  10. package/dist/__tests__/capacity/wham.test.js.map +1 -0
  11. package/dist/__tests__/utils/agents.test.js +6 -0
  12. package/dist/__tests__/utils/agents.test.js.map +1 -1
  13. package/dist/adapters/AgentAdapter.d.ts +1 -1
  14. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  15. package/dist/adapters/AgentAdapter.js.map +1 -1
  16. package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
  17. package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
  18. package/dist/adapters/AntigravityCliAdapter.js +273 -0
  19. package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
  20. package/dist/adapters/KiroAdapter.d.ts +46 -0
  21. package/dist/adapters/KiroAdapter.d.ts.map +1 -0
  22. package/dist/adapters/KiroAdapter.js +344 -0
  23. package/dist/adapters/KiroAdapter.js.map +1 -0
  24. package/dist/adapters/index.d.ts +2 -0
  25. package/dist/adapters/index.d.ts.map +1 -1
  26. package/dist/adapters/index.js +2 -0
  27. package/dist/adapters/index.js.map +1 -1
  28. package/dist/capacity/claude.d.ts +17 -0
  29. package/dist/capacity/claude.d.ts.map +1 -0
  30. package/dist/capacity/claude.js +271 -0
  31. package/dist/capacity/claude.js.map +1 -0
  32. package/dist/capacity/codex.d.ts +1 -2
  33. package/dist/capacity/codex.d.ts.map +1 -1
  34. package/dist/capacity/codex.js +5 -47
  35. package/dist/capacity/codex.js.map +1 -1
  36. package/dist/capacity/index.d.ts +11 -0
  37. package/dist/capacity/index.d.ts.map +1 -1
  38. package/dist/capacity/index.js +17 -0
  39. package/dist/capacity/index.js.map +1 -1
  40. package/dist/capacity/openai.d.ts +48 -0
  41. package/dist/capacity/openai.d.ts.map +1 -0
  42. package/dist/capacity/openai.js +312 -0
  43. package/dist/capacity/openai.js.map +1 -0
  44. package/dist/capacity/wham.d.ts +16 -0
  45. package/dist/capacity/wham.d.ts.map +1 -0
  46. package/dist/capacity/wham.js +66 -0
  47. package/dist/capacity/wham.js.map +1 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/readiness/AgentReadiness.d.ts +1 -1
  53. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  54. package/dist/readiness/AgentReadiness.js.map +1 -1
  55. package/dist/runtime/ManagedAgentRuntime.js +3 -1
  56. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  57. package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
  58. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  59. package/dist/utils/agents.d.ts +1 -1
  60. package/dist/utils/agents.d.ts.map +1 -1
  61. package/dist/utils/agents.js +11 -0
  62. package/dist/utils/agents.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
  65. package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
  66. package/src/__tests__/capacity/claude.test.ts +467 -0
  67. package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
  68. package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
  69. package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
  70. package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
  71. package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
  72. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
  73. package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
  74. package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
  75. package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
  76. package/src/__tests__/capacity/openai.test.ts +570 -0
  77. package/src/__tests__/capacity/wham.test.ts +104 -0
  78. package/src/__tests__/utils/agents.test.ts +7 -0
  79. package/src/adapters/AgentAdapter.ts +2 -0
  80. package/src/adapters/AntigravityCliAdapter.ts +356 -0
  81. package/src/adapters/KiroAdapter.ts +456 -0
  82. package/src/adapters/index.ts +2 -0
  83. package/src/capacity/claude.ts +313 -0
  84. package/src/capacity/codex.ts +25 -134
  85. package/src/capacity/index.ts +33 -4
  86. package/src/capacity/openai.ts +361 -0
  87. package/src/capacity/wham.ts +84 -0
  88. package/src/index.ts +10 -1
  89. package/src/readiness/AgentReadiness.ts +4 -1
  90. package/src/runtime/ManagedAgentRuntime.ts +2 -0
  91. package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
  92. package/src/utils/agents.ts +11 -1
@@ -0,0 +1,356 @@
1
+ import * as path from "path";
2
+ import type {
3
+ AgentAdapter,
4
+ AgentInfo,
5
+ ProcessInfo,
6
+ ConversationMessage,
7
+ SessionSummary,
8
+ ListSessionsOptions,
9
+ } from "./AgentAdapter.js";
10
+ import { AgentStatus } from "./AgentAdapter.js";
11
+ import { listAgentProcesses, enrichProcesses } from "../utils/process.js";
12
+ import { safeReadFile, safeStat } from "../utils/session.js";
13
+ import { generateAgentName } from "../utils/matching.js";
14
+
15
+ /**
16
+ * Antigravity CLI Adapter
17
+ *
18
+ * Detects running Antigravity CLI agents (Google's Gemini-family `agy` CLI) by:
19
+ * 1. Finding running `agy` processes via shared listAgentProcesses() — Antigravity
20
+ * ships a native binary (argv[0] basename `agy`).
21
+ * 2. Resolving each live process to its conversation via
22
+ * ~/.gemini/antigravity-cli/cache/last_conversations.json, which the CLI
23
+ * maintains as a `{ <cwd>: <conversationId> }` map of the current conversation
24
+ * per workspace. The process cwd is the join key.
25
+ * 3. Reading the transcript from
26
+ * brain/<conversationId>/.system_generated/logs/transcript.jsonl. User turns are
27
+ * USER_INPUT records (prompt inside <USER_REQUEST>...</USER_REQUEST>) and the
28
+ * model's reply is a PLANNER_RESPONSE record; tool calls (RUN_COMMAND, ...) are
29
+ * skipped. The last user turn is the summary; each record's created_at gives the
30
+ * last-activity time.
31
+ *
32
+ * This is the runtime/agent side of Antigravity; it is independent of the
33
+ * `antigravity` environment (which configures the Antigravity IDE), the same way
34
+ * GeminiCliAdapter is independent of the `gemini` environment.
35
+ */
36
+
37
+ const REGISTRY_FILE = path.join("cache", "last_conversations.json");
38
+ const BRAIN_DIR = "brain";
39
+ const TRANSCRIPT_REL = path.join(".system_generated", "logs", "transcript.jsonl");
40
+ const IDLE_THRESHOLD_MINUTES = 5;
41
+
42
+ /** One line of transcript.jsonl. */
43
+ interface TranscriptRecord {
44
+ source?: string;
45
+ type?: string;
46
+ created_at?: string;
47
+ content?: unknown;
48
+ }
49
+
50
+ interface TranscriptScan {
51
+ messages: ConversationMessage[];
52
+ firstUserMessage?: string;
53
+ lastUserMessage?: string;
54
+ lastRole?: ConversationMessage["role"];
55
+ lastActive?: Date;
56
+ }
57
+
58
+ /** Parsed state for a single conversation. */
59
+ interface AntigravitySession {
60
+ sessionId: string;
61
+ projectPath: string;
62
+ summary: string;
63
+ sessionStart: Date;
64
+ lastActive: Date;
65
+ firstUserMessage?: string;
66
+ lastUserMessage?: string;
67
+ lastRole?: ConversationMessage["role"];
68
+ }
69
+
70
+ export class AntigravityCliAdapter implements AgentAdapter {
71
+ readonly type = "antigravity_cli" as const;
72
+
73
+ private base: string;
74
+
75
+ constructor() {
76
+ // ANTIGRAVITY_CLI_HOME overrides the ~/.gemini/antigravity-cli base dir.
77
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
78
+ this.base =
79
+ process.env.ANTIGRAVITY_CLI_HOME || path.join(homeDir, ".gemini", "antigravity-cli");
80
+ }
81
+
82
+ canHandle(processInfo: ProcessInfo): boolean {
83
+ return this.isAgyExecutable(processInfo.command);
84
+ }
85
+
86
+ private isAgyExecutable(command: string): boolean {
87
+ const executable = command.trim().split(/\s+/)[0] || "";
88
+ const base = path.basename(executable).toLowerCase();
89
+ return base === "agy" || base === "agy.exe";
90
+ }
91
+
92
+ async detectAgents(): Promise<AgentInfo[]> {
93
+ const processes = enrichProcesses(listAgentProcesses("agy"));
94
+ if (processes.length === 0) {
95
+ return [];
96
+ }
97
+
98
+ // last_conversations.json maps a workspace cwd to its current
99
+ // conversation id; the live process cwd is the join key.
100
+ const cwdToConversation = this.readRegistry();
101
+
102
+ const agents: AgentInfo[] = [];
103
+ for (const proc of processes) {
104
+ const cwd = proc.cwd || "";
105
+ const conversationId = cwd ? cwdToConversation.get(cwd) : undefined;
106
+ const session = conversationId ? this.readSession(conversationId, cwd) : null;
107
+
108
+ if (session) {
109
+ agents.push(this.mapSessionToAgent(session, proc));
110
+ } else {
111
+ agents.push(this.mapProcessOnlyAgent(proc, cwd));
112
+ }
113
+ }
114
+
115
+ return agents;
116
+ }
117
+
118
+ /**
119
+ * Read cache/last_conversations.json into a cwd -> conversationId map. The
120
+ * CLI rewrites this as the current conversation per workspace changes.
121
+ */
122
+ private readRegistry(): Map<string, string> {
123
+ const map = new Map<string, string>();
124
+ const content = safeReadFile(path.join(this.base, REGISTRY_FILE));
125
+ if (content === undefined) return map;
126
+
127
+ let parsed: unknown;
128
+ try {
129
+ parsed = JSON.parse(content);
130
+ } catch {
131
+ return map;
132
+ }
133
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return map;
134
+
135
+ for (const [cwd, conversationId] of Object.entries(parsed as Record<string, unknown>)) {
136
+ if (cwd && typeof conversationId === "string" && conversationId) {
137
+ map.set(cwd, conversationId);
138
+ }
139
+ }
140
+ return map;
141
+ }
142
+
143
+ private transcriptPath(conversationId: string): string {
144
+ return path.join(this.base, BRAIN_DIR, conversationId, TRANSCRIPT_REL);
145
+ }
146
+
147
+ private mapSessionToAgent(session: AntigravitySession, processInfo: ProcessInfo): AgentInfo {
148
+ const projectPath = session.projectPath || processInfo.cwd || "";
149
+ return {
150
+ name: generateAgentName(projectPath, processInfo.pid),
151
+ type: this.type,
152
+ status: this.determineStatus(session),
153
+ summary: session.summary || "Antigravity CLI session active",
154
+ pid: processInfo.pid,
155
+ projectPath,
156
+ sessionId: session.sessionId,
157
+ lastActive: session.lastActive,
158
+ sessionFilePath: this.transcriptPath(session.sessionId),
159
+ };
160
+ }
161
+
162
+ private mapProcessOnlyAgent(processInfo: ProcessInfo, cwd: string): AgentInfo {
163
+ const projectPath = cwd || processInfo.cwd || "";
164
+ return {
165
+ name: generateAgentName(projectPath, processInfo.pid),
166
+ type: this.type,
167
+ status: AgentStatus.RUNNING,
168
+ summary: "Antigravity CLI process running",
169
+ pid: processInfo.pid,
170
+ projectPath,
171
+ sessionId: `pid-${processInfo.pid}`,
172
+ lastActive: new Date(),
173
+ };
174
+ }
175
+
176
+ getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
177
+ return this.parseTranscript(
178
+ this.resolveTranscriptPath(sessionFilePath),
179
+ options?.verbose ?? false,
180
+ ).messages;
181
+ }
182
+
183
+ async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
184
+ const filterCwd = opts?.cwd;
185
+ const summaries: SessionSummary[] = [];
186
+
187
+ for (const [cwd, conversationId] of this.readRegistry()) {
188
+ if (filterCwd !== undefined && cwd !== filterCwd) continue;
189
+
190
+ const session = this.readSession(conversationId, cwd);
191
+ if (!session) continue;
192
+
193
+ summaries.push({
194
+ type: this.type,
195
+ sessionId: session.sessionId,
196
+ cwd: session.projectPath || cwd,
197
+ firstUserMessage: session.firstUserMessage || "",
198
+ lastActive: session.lastActive,
199
+ startedAt: session.sessionStart,
200
+ sessionFilePath: this.transcriptPath(conversationId),
201
+ });
202
+ }
203
+
204
+ return summaries;
205
+ }
206
+
207
+ // --- Session parsing (transcript.jsonl) ---
208
+
209
+ /**
210
+ * Parse a conversation into an {@link AntigravitySession} from its
211
+ * transcript.jsonl. Returns null when the transcript is missing — i.e. there
212
+ * is no real session to surface.
213
+ */
214
+ private readSession(conversationId: string, defaultCwd: string): AntigravitySession | null {
215
+ const transcriptPath = this.transcriptPath(conversationId);
216
+ const stat = safeStat(transcriptPath);
217
+ if (!stat) return null;
218
+
219
+ const scan = this.parseTranscript(transcriptPath, false);
220
+ const lastActive = scan.lastActive || stat.mtime;
221
+
222
+ return {
223
+ sessionId: conversationId,
224
+ projectPath: defaultCwd || "",
225
+ summary: scan.lastUserMessage || "Antigravity CLI session active",
226
+ sessionStart: stat.birthtime || lastActive,
227
+ lastActive,
228
+ firstUserMessage: scan.firstUserMessage,
229
+ lastUserMessage: scan.lastUserMessage,
230
+ lastRole: scan.lastRole,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Determine agent status from parsed session state.
236
+ *
237
+ * - past the idle threshold → IDLE
238
+ * - last transcript turn is an assistant message → WAITING (awaiting user)
239
+ * - otherwise (last turn was a user message, or unknown) → RUNNING
240
+ */
241
+ private determineStatus(session: AntigravitySession): AgentStatus {
242
+ const diffMinutes = (Date.now() - session.lastActive.getTime()) / 60000;
243
+ if (diffMinutes > IDLE_THRESHOLD_MINUTES) {
244
+ return AgentStatus.IDLE;
245
+ }
246
+ if (session.lastRole === "assistant") {
247
+ return AgentStatus.WAITING;
248
+ }
249
+ return AgentStatus.RUNNING;
250
+ }
251
+
252
+ /**
253
+ * Single pass over transcript.jsonl. Each line is a
254
+ * { source, type, created_at, content } record. User turns are
255
+ * `type: 'USER_INPUT'` with the real prompt wrapped in
256
+ * <USER_REQUEST>...</USER_REQUEST>; the model's reply is `type:
257
+ * 'PLANNER_RESPONSE'`. Other MODEL records (tool calls like RUN_COMMAND) and
258
+ * SYSTEM records (conversation history, checkpoints) are execution detail and
259
+ * are skipped unless verbose.
260
+ */
261
+ private parseTranscript(transcriptPath: string, verbose: boolean): TranscriptScan {
262
+ const empty: TranscriptScan = { messages: [] };
263
+ const content = safeReadFile(transcriptPath);
264
+ if (content === undefined) return empty;
265
+
266
+ const messages: ConversationMessage[] = [];
267
+ let lastRole: ConversationMessage["role"] | undefined;
268
+ let lastActive: Date | undefined;
269
+
270
+ for (const line of content.trim().split("\n")) {
271
+ if (!line.trim()) continue;
272
+
273
+ let record: TranscriptRecord;
274
+ try {
275
+ record = JSON.parse(line);
276
+ } catch {
277
+ continue;
278
+ }
279
+
280
+ const at = this.parseTimestamp(record.created_at);
281
+ if (at && (!lastActive || at.getTime() > lastActive.getTime())) lastActive = at;
282
+
283
+ const text = this.extractText(record.content);
284
+ if (record.type === "USER_INPUT") {
285
+ const request = this.extractUserRequest(text);
286
+ if (request === null) continue; // not a real prompt
287
+ messages.push({ role: "user", content: request });
288
+ lastRole = "user";
289
+ } else if (record.type === "PLANNER_RESPONSE") {
290
+ // The model's user-facing reply. Other MODEL records (tool calls
291
+ // such as RUN_COMMAND, empty planning steps) are execution detail,
292
+ // not conversation, so they are excluded from the normal view.
293
+ if (!text) continue;
294
+ messages.push({ role: "assistant", content: text });
295
+ lastRole = "assistant";
296
+ } else if (verbose) {
297
+ // Tool calls (MODEL/RUN_COMMAND, ...) and SYSTEM records surface
298
+ // only in verbose mode.
299
+ if (!text) continue;
300
+ messages.push({ role: "system", content: text });
301
+ }
302
+ }
303
+
304
+ const userTurns = messages.filter((m) => m.role === "user");
305
+ return {
306
+ messages,
307
+ firstUserMessage: userTurns[0]?.content,
308
+ lastUserMessage: userTurns[userTurns.length - 1]?.content,
309
+ lastRole,
310
+ lastActive,
311
+ };
312
+ }
313
+
314
+ /** Flatten a transcript record's content (string or text-block array) to text. */
315
+ private extractText(content: unknown): string {
316
+ if (typeof content === "string") return content;
317
+ if (Array.isArray(content)) {
318
+ return content
319
+ .map((block) =>
320
+ block &&
321
+ typeof block === "object" &&
322
+ typeof (block as { text?: unknown }).text === "string"
323
+ ? (block as { text: string }).text
324
+ : "",
325
+ )
326
+ .join("");
327
+ }
328
+ return "";
329
+ }
330
+
331
+ /**
332
+ * Extract the prompt inside <USER_REQUEST>...</USER_REQUEST>. Returns null when
333
+ * the record has no such tag (context injection rather than a prompt). Falls
334
+ * back to the trimmed text when a USER_INPUT record has no wrapper.
335
+ */
336
+ private extractUserRequest(text: string): string | null {
337
+ const match = text.match(/<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/);
338
+ if (match) return match[1].trim();
339
+ const trimmed = text.trim();
340
+ return trimmed ? trimmed : null;
341
+ }
342
+
343
+ /** Resolve a conversation dir, transcript path, or bare id to the transcript file. */
344
+ private resolveTranscriptPath(sessionPath: string): string {
345
+ if (sessionPath.endsWith(".jsonl")) return sessionPath;
346
+ // A bare conversation id (no separator) resolves under the brain dir.
347
+ if (!sessionPath.includes(path.sep)) return this.transcriptPath(sessionPath);
348
+ return path.join(sessionPath, TRANSCRIPT_REL);
349
+ }
350
+
351
+ private parseTimestamp(value?: string): Date | null {
352
+ if (!value) return null;
353
+ const ts = new Date(value);
354
+ return Number.isNaN(ts.getTime()) ? null : ts;
355
+ }
356
+ }