@ai-devkit/agent-manager 0.40.0 → 0.42.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 (115) hide show
  1. package/dist/AgentManager.d.ts +12 -0
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +45 -10
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +111 -0
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/fixtures.test.js +218 -0
  8. package/dist/__tests__/fixtures.test.js.map +1 -0
  9. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
  10. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
  11. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
  12. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
  13. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
  14. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
  15. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
  16. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
  17. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
  18. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
  19. package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
  20. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
  21. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
  22. package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
  23. package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
  24. package/dist/__tests__/readiness-fixtures.test.js +54 -0
  25. package/dist/__tests__/readiness-fixtures.test.js.map +1 -0
  26. package/dist/__tests__/utils/process.test.js +71 -1
  27. package/dist/__tests__/utils/process.test.js.map +1 -1
  28. package/dist/adapters/AgentAdapter.d.ts +8 -1
  29. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  30. package/dist/adapters/AgentAdapter.js +2 -1
  31. package/dist/adapters/AgentAdapter.js.map +1 -1
  32. package/dist/fixtures/bundle.d.ts +67 -0
  33. package/dist/fixtures/bundle.d.ts.map +1 -0
  34. package/dist/fixtures/bundle.js +86 -0
  35. package/dist/fixtures/bundle.js.map +1 -0
  36. package/dist/fixtures/capture.d.ts +11 -0
  37. package/dist/fixtures/capture.d.ts.map +1 -0
  38. package/dist/fixtures/capture.js +318 -0
  39. package/dist/fixtures/capture.js.map +1 -0
  40. package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
  41. package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
  42. package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
  43. package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
  44. package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
  45. package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
  46. package/dist/harnesses/index.d.ts.map +1 -1
  47. package/dist/harnesses/index.js +3 -1
  48. package/dist/harnesses/index.js.map +1 -1
  49. package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
  50. package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
  51. package/dist/harnesses/muse/MuseAdapter.js +122 -0
  52. package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
  53. package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
  54. package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
  55. package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
  56. package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
  57. package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
  58. package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
  59. package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
  60. package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
  61. package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
  62. package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
  63. package/dist/harnesses/muse/MuseSessionParser.js +350 -0
  64. package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
  65. package/dist/harnesses/muse/readiness.d.ts +3 -0
  66. package/dist/harnesses/muse/readiness.d.ts.map +1 -0
  67. package/dist/harnesses/muse/readiness.js +37 -0
  68. package/dist/harnesses/muse/readiness.js.map +1 -0
  69. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
  70. package/dist/harnesses/readiness/AgentReadiness.js +3 -1
  71. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
  72. package/dist/harnesses/readiness/types.d.ts +1 -1
  73. package/dist/harnesses/readiness/types.d.ts.map +1 -1
  74. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
  75. package/dist/harnesses/runtimeProfiles.js +2 -1
  76. package/dist/harnesses/runtimeProfiles.js.map +1 -1
  77. package/dist/harnesses/shared.d.ts +1 -1
  78. package/dist/harnesses/shared.d.ts.map +1 -1
  79. package/dist/harnesses/shared.js +7 -6
  80. package/dist/harnesses/shared.js.map +1 -1
  81. package/dist/utils/process.d.ts +23 -1
  82. package/dist/utils/process.d.ts.map +1 -1
  83. package/dist/utils/process.js +44 -8
  84. package/dist/utils/process.js.map +1 -1
  85. package/package.json +1 -1
  86. package/src/AgentManager.ts +71 -9
  87. package/src/__tests__/AgentManager.test.ts +84 -0
  88. package/src/__tests__/fixtures.test.ts +245 -0
  89. package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
  90. package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
  91. package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
  92. package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
  93. package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
  94. package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
  95. package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
  96. package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
  97. package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
  98. package/src/__tests__/readiness-fixtures.test.ts +79 -0
  99. package/src/__tests__/utils/process.test.ts +30 -0
  100. package/src/adapters/AgentAdapter.ts +9 -0
  101. package/src/fixtures/bundle.ts +130 -0
  102. package/src/fixtures/capture.ts +364 -0
  103. package/src/harnesses/devin/DevinAdapter.ts +3 -15
  104. package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
  105. package/src/harnesses/devin/DevinSessionParser.ts +3 -12
  106. package/src/harnesses/index.ts +2 -0
  107. package/src/harnesses/muse/MuseAdapter.ts +157 -0
  108. package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
  109. package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
  110. package/src/harnesses/muse/MuseSessionParser.ts +435 -0
  111. package/src/harnesses/muse/readiness.ts +38 -0
  112. package/src/harnesses/readiness/AgentReadiness.ts +2 -0
  113. package/src/harnesses/runtimeProfiles.ts +1 -0
  114. package/src/harnesses/shared.ts +11 -6
  115. package/src/utils/process.ts +61 -9
@@ -0,0 +1,286 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { ProcessInfo } from "../../adapters/AgentAdapter.js";
4
+ import { matchProcessesToSessions, type MatchResult } from "../../utils/matching.js";
5
+ import {
6
+ isDirectory,
7
+ isSafePathSegment,
8
+ safeReaddir,
9
+ safeStat,
10
+ type SessionFile,
11
+ } from "../../utils/session.js";
12
+ import { homeDir } from "../shared.js";
13
+
14
+ /** Entry in `~/.local/share/muse/runtime/muse/sessions/<session-id>.json`. */
15
+ export interface MuseRuntimeEntry {
16
+ sessionId: string;
17
+ pid: number;
18
+ }
19
+
20
+ /** A live Muse process paired with its transcript through the runtime file. */
21
+ export interface MuseDirectMatch {
22
+ process: ProcessInfo;
23
+ sessionFile: SessionFile;
24
+ }
25
+
26
+ export interface MuseProcessSessionMatches {
27
+ direct: MuseDirectMatch[];
28
+ legacyMatches: MatchResult[];
29
+ /** Muse processes no strategy could pair (become process-only agents). */
30
+ fallback: ProcessInfo[];
31
+ }
32
+
33
+ export interface MuseSessionLocatorOptions {
34
+ homeDir?: string;
35
+ runtimeDir?: string;
36
+ archiveDir?: string;
37
+ }
38
+
39
+ /** Session ids are UUID-shaped; anything else is never a session dir. */
40
+ const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41
+
42
+ /** Maximum |process start − transcript birth| before a pid match is recycled. */
43
+ const PID_STALENESS_MS = 60000;
44
+
45
+ function parsePidHint(value: unknown): number | null {
46
+ if (typeof value !== "string") return null;
47
+ const match = /^pid=(\d+)$/.exec(value.trim());
48
+ return match ? Number(match[1]) : null;
49
+ }
50
+
51
+ function parseRuntimeEntry(filePath: string): MuseRuntimeEntry | null {
52
+ let raw: string;
53
+ try {
54
+ raw = fs.readFileSync(filePath, "utf-8");
55
+ } catch {
56
+ return null;
57
+ }
58
+ let parsed: unknown;
59
+ try {
60
+ parsed = JSON.parse(raw);
61
+ } catch {
62
+ return null;
63
+ }
64
+ if (!parsed || typeof parsed !== "object") return null;
65
+ const entry = parsed as { session_id?: unknown; process_generation_hint?: unknown };
66
+ const sessionId = typeof entry.session_id === "string" ? entry.session_id : null;
67
+ const pid = parsePidHint(entry.process_generation_hint);
68
+ if (!sessionId || !SESSION_ID_PATTERN.test(sessionId) || pid === null) return null;
69
+ return { sessionId, pid };
70
+ }
71
+
72
+ /** Head-read a transcript for its recorded workspace root (bounded, cheap). */
73
+ function readWorkspaceRootHead(filePath: string, maxBytes = 8192): string | null {
74
+ let fd: number;
75
+ try {
76
+ fd = fs.openSync(filePath, "r");
77
+ } catch {
78
+ return null;
79
+ }
80
+ try {
81
+ const stat = fs.fstatSync(fd);
82
+ const buf = Buffer.alloc(Math.min(maxBytes, stat.size));
83
+ fs.readSync(fd, buf, 0, buf.length, 0);
84
+ const text = buf.toString("utf-8");
85
+ for (const line of text.split("\n")) {
86
+ if (!line.includes("runtime.session.metadata")) continue;
87
+ try {
88
+ const outer = JSON.parse(line) as {
89
+ record_json?: string;
90
+ payload?: { record?: { workspace_root?: unknown } };
91
+ };
92
+ const inner =
93
+ typeof outer.record_json === "string"
94
+ ? (JSON.parse(outer.record_json) as typeof outer)
95
+ : outer;
96
+ const root = inner.payload?.record?.workspace_root;
97
+ if (typeof root === "string" && root) return root;
98
+ } catch {
99
+ /* keep scanning */
100
+ }
101
+ }
102
+ return null;
103
+ } catch {
104
+ return null;
105
+ } finally {
106
+ try {
107
+ fs.closeSync(fd);
108
+ } catch {
109
+ /* best effort */
110
+ }
111
+ }
112
+ }
113
+
114
+ export class MuseSessionLocator {
115
+ private readonly home: string;
116
+ private readonly runtimeDir: string;
117
+ private readonly archiveDir: string;
118
+
119
+ constructor(options: MuseSessionLocatorOptions = {}) {
120
+ this.home = options.homeDir ?? homeDir();
121
+ const store = path.join(this.home, ".local", "share", "muse");
122
+ this.runtimeDir = options.runtimeDir ?? path.join(store, "runtime", "muse", "sessions");
123
+ this.archiveDir = options.archiveDir ?? path.join(store, "sessions");
124
+ }
125
+
126
+ /**
127
+ * Pair live Muse processes with transcripts.
128
+ *
129
+ * Two staged strategies, each handing unmatched processes onward:
130
+ * 1. Runtime-file matching via `runtime/muse/sessions/<id>.json`
131
+ * (authoritative; guarded against recycled pids).
132
+ * 2. Legacy cwd+birthtime heuristic over recent transcripts.
133
+ */
134
+ matchRunningProcesses(processes: ProcessInfo[]): MuseProcessSessionMatches {
135
+ // Callers pass muse processes only (the adapter pre-filters via canHandle).
136
+ const { direct, fallback: noDirect } = this.tryRuntimeFileMatching(processes);
137
+ const legacySessions = this.discoverLiveSessions(noDirect);
138
+ const legacyMatches =
139
+ noDirect.length > 0 && legacySessions.length > 0
140
+ ? matchProcessesToSessions(noDirect, legacySessions)
141
+ : [];
142
+ const matchedPids = new Set([
143
+ ...direct.map((d) => d.process.pid),
144
+ ...legacyMatches.map((m) => m.process.pid),
145
+ ]);
146
+ return {
147
+ direct,
148
+ legacyMatches,
149
+ fallback: noDirect.filter((p) => !matchedPids.has(p.pid)),
150
+ };
151
+ }
152
+
153
+ private tryRuntimeFileMatching(processes: ProcessInfo[]): {
154
+ direct: MuseDirectMatch[];
155
+ fallback: ProcessInfo[];
156
+ } {
157
+ const byPid = new Map<number, ProcessInfo>();
158
+ for (const proc of processes) {
159
+ if (!byPid.has(proc.pid)) byPid.set(proc.pid, proc);
160
+ }
161
+ const direct: MuseDirectMatch[] = [];
162
+ const usedPids = new Set<number>();
163
+ if (!isDirectory(this.runtimeDir)) return { direct, fallback: processes };
164
+
165
+ for (const name of safeReaddir(this.runtimeDir)) {
166
+ if (!name.endsWith(".json")) continue;
167
+ const entry = parseRuntimeEntry(path.join(this.runtimeDir, name));
168
+ if (!entry) continue;
169
+ const proc = byPid.get(entry.pid);
170
+ if (!proc || usedPids.has(proc.pid)) continue;
171
+ const transcript = this.findTranscriptById(entry.sessionId);
172
+ if (!transcript) continue;
173
+ // Guard against pid recycling: the live process must have started
174
+ // around the transcript's creation.
175
+ const birth = safeStat(transcript)?.birthtimeMs;
176
+ const start = proc.startTime?.getTime();
177
+ if (birth === undefined || start === undefined) continue;
178
+ if (Math.abs(start - birth) > PID_STALENESS_MS) continue;
179
+ usedPids.add(proc.pid);
180
+ direct.push({
181
+ process: proc,
182
+ sessionFile: {
183
+ sessionId: entry.sessionId,
184
+ filePath: transcript,
185
+ projectDir: path.dirname(transcript),
186
+ birthtimeMs: birth,
187
+ resolvedCwd: proc.cwd ?? "",
188
+ },
189
+ });
190
+ }
191
+ return { direct, fallback: processes.filter((p) => !usedPids.has(p.pid)) };
192
+ }
193
+
194
+ /** Recent transcripts for legacy cwd+birthtime matching (today-first, bounded). */
195
+ discoverLiveSessions(processes: ProcessInfo[]): SessionFile[] {
196
+ const cwds = new Set(processes.map((p) => p.cwd).filter((cwd): cwd is string => !!cwd));
197
+ if (cwds.size === 0 || !isDirectory(this.archiveDir)) return [];
198
+ const dayDirs = this.listDayDirs().slice(-2);
199
+ const sessionFiles: SessionFile[] = [];
200
+ for (const dayDir of dayDirs) {
201
+ for (const name of safeReaddir(dayDir)) {
202
+ if (!SESSION_ID_PATTERN.test(name)) continue;
203
+ const filePath = path.join(dayDir, name, "session.jsonl");
204
+ const stat = safeStat(filePath);
205
+ if (!stat?.isFile() || !Number.isFinite(stat.birthtimeMs) || stat.birthtimeMs <= 0) {
206
+ continue;
207
+ }
208
+ const workspaceRoot = readWorkspaceRootHead(filePath);
209
+ if (!workspaceRoot || !cwds.has(workspaceRoot)) continue;
210
+ sessionFiles.push({
211
+ sessionId: name,
212
+ filePath,
213
+ projectDir: path.dirname(filePath),
214
+ birthtimeMs: stat.birthtimeMs,
215
+ resolvedCwd: workspaceRoot,
216
+ });
217
+ }
218
+ }
219
+ return sessionFiles;
220
+ }
221
+
222
+ /**
223
+ * Every archived transcript: walks `sessions/yyyy/mm/dd/<uuid>/session.jsonl`.
224
+ * Only directories matching the session-id pattern that contain a transcript
225
+ * are returned; `subagent/`, logs, and databases never match.
226
+ */
227
+ discoverHistoricalSessionFiles(): Array<{ filePath: string; defaultCwd: string }> {
228
+ const out: Array<{ filePath: string; defaultCwd: string }> = [];
229
+ for (const filePath of this.walkTranscripts()) {
230
+ out.push({ filePath, defaultCwd: "" });
231
+ }
232
+ return out;
233
+ }
234
+
235
+ findHistoricalSessionFilesById(
236
+ sessionId: string,
237
+ ): Array<{ filePath: string; defaultCwd: string }> {
238
+ if (!isSafePathSegment(sessionId) || !SESSION_ID_PATTERN.test(sessionId)) return [];
239
+ const filePath = this.findTranscriptById(sessionId);
240
+ return filePath ? [{ filePath, defaultCwd: "" }] : [];
241
+ }
242
+
243
+ /** Exact transcript lookup by session id across dated dirs. */
244
+ findTranscriptById(sessionId: string): string | null {
245
+ if (!isSafePathSegment(sessionId) || !SESSION_ID_PATTERN.test(sessionId)) return null;
246
+ for (const dayDir of this.listDayDirs()) {
247
+ const filePath = path.join(dayDir, sessionId, "session.jsonl");
248
+ if (safeStat(filePath)?.isFile()) return filePath;
249
+ }
250
+ return null;
251
+ }
252
+
253
+ private walkTranscripts(): string[] {
254
+ const out: string[] = [];
255
+ for (const dayDir of this.listDayDirs()) {
256
+ for (const name of safeReaddir(dayDir)) {
257
+ if (!SESSION_ID_PATTERN.test(name)) continue;
258
+ const filePath = path.join(dayDir, name, "session.jsonl");
259
+ if (safeStat(filePath)?.isFile()) out.push(filePath);
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+
265
+ /** Dated directories `sessions/yyyy/mm/dd`, each level guarded by a pattern. */
266
+ private listDayDirs(): string[] {
267
+ const out: string[] = [];
268
+ if (!isDirectory(this.archiveDir)) return out;
269
+ for (const yyyy of safeReaddir(this.archiveDir)) {
270
+ if (!/^\d{4}$/.test(yyyy)) continue;
271
+ const yDir = path.join(this.archiveDir, yyyy);
272
+ if (!isDirectory(yDir)) continue;
273
+ for (const mm of safeReaddir(yDir)) {
274
+ if (!/^\d{2}$/.test(mm)) continue;
275
+ const mDir = path.join(yDir, mm);
276
+ if (!isDirectory(mDir)) continue;
277
+ for (const dd of safeReaddir(mDir)) {
278
+ if (!/^\d{2}$/.test(dd)) continue;
279
+ const dDir = path.join(mDir, dd);
280
+ if (isDirectory(dDir)) out.push(dDir);
281
+ }
282
+ }
283
+ }
284
+ return out;
285
+ }
286
+ }