@ai-devkit/agent-manager 0.41.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.
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +25 -9
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/fixtures.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
- package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +71 -1
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +8 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/fixtures/capture.d.ts.map +1 -1
- package/dist/fixtures/capture.js +5 -4
- package/dist/fixtures/capture.js.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
- package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAdapter.js +122 -0
- package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
- package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
- package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.js +350 -0
- package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
- package/dist/harnesses/muse/readiness.d.ts +3 -0
- package/dist/harnesses/muse/readiness.d.ts.map +1 -0
- package/dist/harnesses/muse/readiness.js +37 -0
- package/dist/harnesses/muse/readiness.js.map +1 -0
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/harnesses/shared.d.ts +1 -1
- package/dist/harnesses/shared.d.ts.map +1 -1
- package/dist/harnesses/shared.js +6 -5
- package/dist/harnesses/shared.js.map +1 -1
- package/dist/utils/process.d.ts +23 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -8
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +34 -8
- package/src/__tests__/fixtures.test.ts +15 -17
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
- package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
- package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
- package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
- package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
- package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
- package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
- package/src/__tests__/utils/process.test.ts +30 -0
- package/src/adapters/AgentAdapter.ts +9 -0
- package/src/fixtures/capture.ts +32 -17
- package/src/harnesses/devin/DevinAdapter.ts +3 -15
- package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
- package/src/harnesses/devin/DevinSessionParser.ts +3 -12
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/muse/MuseAdapter.ts +157 -0
- package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
- package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
- package/src/harnesses/muse/MuseSessionParser.ts +435 -0
- package/src/harnesses/muse/readiness.ts +38 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/harnesses/shared.ts +10 -5
- package/src/utils/process.ts +61 -9
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type { ConversationMessage, ConversationOptions } from "../../adapters/AgentAdapter.js";
|
|
4
|
+
import { AgentStatus } from "../../adapters/AgentAdapter.js";
|
|
5
|
+
import {
|
|
6
|
+
IncrementalJsonlSummary,
|
|
7
|
+
reduceJsonlContent,
|
|
8
|
+
type JsonlSummaryReducer,
|
|
9
|
+
} from "../../utils/IncrementalJsonlSummary.js";
|
|
10
|
+
import { JsonlTailReader, normalizeTail } from "../../utils/jsonlTail.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single unwrapped inner record from a Muse session frame.
|
|
14
|
+
*
|
|
15
|
+
* Each session.jsonl line is one of: a direct inner record, a
|
|
16
|
+
* `{ record_json: string }` envelope, or a `retained_frame` wrapper whose
|
|
17
|
+
* `children[]` carry `record_json` strings. Inner records dispatch on
|
|
18
|
+
* `payload_type`; timestamps (`recorded_at`) are microseconds.
|
|
19
|
+
*/
|
|
20
|
+
export interface MuseInnerRecord {
|
|
21
|
+
stream?: { kind?: string; id?: string };
|
|
22
|
+
sequence?: number;
|
|
23
|
+
recorded_at?: number;
|
|
24
|
+
record_type?: string;
|
|
25
|
+
payload_type?: string;
|
|
26
|
+
payload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parsed session state extracted from a Muse transcript. */
|
|
30
|
+
export interface MuseSession {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
projectPath: string;
|
|
33
|
+
/** Last display name from `session.name.changed`, when present. */
|
|
34
|
+
displayName?: string;
|
|
35
|
+
modelId?: string;
|
|
36
|
+
sessionStart: Date;
|
|
37
|
+
lastActive: Date;
|
|
38
|
+
/** Last conversation signal: user-intent, assistant reply, approval, tool batch… */
|
|
39
|
+
lastSignal?: string;
|
|
40
|
+
lastUserMessage?: string;
|
|
41
|
+
/** First meaningful user prompt in the session. */
|
|
42
|
+
firstUserMessage?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface MuseSummaryState {
|
|
46
|
+
seenFirstLine: boolean;
|
|
47
|
+
pastHead?: boolean;
|
|
48
|
+
sessionStartMs?: number;
|
|
49
|
+
lastActiveMs?: number;
|
|
50
|
+
workspaceRoot?: string;
|
|
51
|
+
modelId?: string;
|
|
52
|
+
displayName?: string;
|
|
53
|
+
lastSignal?: string;
|
|
54
|
+
lastUserMessage?: string;
|
|
55
|
+
firstUserMessage?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Conversation-relevant shape of one inner record. Both the summary fold and
|
|
60
|
+
* the message reader switch on this instead of re-matching payload shapes.
|
|
61
|
+
*/
|
|
62
|
+
type MuseRecordKind =
|
|
63
|
+
| "metadata"
|
|
64
|
+
| "name-changed"
|
|
65
|
+
| "user-intent"
|
|
66
|
+
| "assistant"
|
|
67
|
+
| "tool-calls"
|
|
68
|
+
| "approval-started"
|
|
69
|
+
| "approval-settled"
|
|
70
|
+
| "input-requested"
|
|
71
|
+
| "input-settled"
|
|
72
|
+
| "run-terminal"
|
|
73
|
+
| "other";
|
|
74
|
+
|
|
75
|
+
function classifyRecord(record: MuseInnerRecord): MuseRecordKind {
|
|
76
|
+
const payload = record.payload;
|
|
77
|
+
if (!isRecord(payload)) return "other";
|
|
78
|
+
switch (record.payload_type) {
|
|
79
|
+
case "runtime.session.metadata":
|
|
80
|
+
return "metadata";
|
|
81
|
+
case "session.name.changed":
|
|
82
|
+
return "name-changed";
|
|
83
|
+
case "runtime.user_intent.accepted":
|
|
84
|
+
case "runtime.user_intent.materialized":
|
|
85
|
+
return "user-intent";
|
|
86
|
+
case "runtime.session":
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
return "other";
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(payload.event) || typeof payload.event.kind !== "string") return "other";
|
|
92
|
+
if (payload.kind === "run") {
|
|
93
|
+
switch (payload.event.kind) {
|
|
94
|
+
case "assistant_message_committed":
|
|
95
|
+
return "assistant";
|
|
96
|
+
case "assistant_tool_calls_committed":
|
|
97
|
+
return "tool-calls";
|
|
98
|
+
case "terminal":
|
|
99
|
+
return "run-terminal";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
switch (payload.event.kind) {
|
|
103
|
+
case "approval_wait.effect.started":
|
|
104
|
+
return "approval-started";
|
|
105
|
+
case "approval_wait.effect.terminal":
|
|
106
|
+
return "approval-settled";
|
|
107
|
+
case "user_input_prompt_requested":
|
|
108
|
+
return "input-requested";
|
|
109
|
+
case "user_input_prompt_settled":
|
|
110
|
+
return "input-settled";
|
|
111
|
+
default:
|
|
112
|
+
return "other";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MICROSECONDS_THRESHOLD = 1_000_000_000_000_000;
|
|
117
|
+
|
|
118
|
+
/** `recorded_at` microseconds → epoch ms; null when invalid. */
|
|
119
|
+
export function museTimestampToMs(value: unknown): number | null {
|
|
120
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
121
|
+
const ms = value >= MICROSECONDS_THRESHOLD ? Math.floor(value / 1000) : value;
|
|
122
|
+
return ms >= 0 ? ms : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Unwrap one raw frame line into zero or more inner records. */
|
|
126
|
+
export function unwrapFrameRecords(line: string): MuseInnerRecord[] {
|
|
127
|
+
let outer: unknown;
|
|
128
|
+
try {
|
|
129
|
+
outer = JSON.parse(line);
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return unwrapOuterRecords(outer);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Unwrap an already-parsed frame value into zero or more inner records. */
|
|
137
|
+
export function unwrapOuterRecords(outer: unknown): MuseInnerRecord[] {
|
|
138
|
+
if (!outer || typeof outer !== "object") return [];
|
|
139
|
+
const obj = outer as { record_json?: unknown; children?: unknown };
|
|
140
|
+
if (typeof obj.record_json === "string") {
|
|
141
|
+
try {
|
|
142
|
+
const inner = JSON.parse(obj.record_json) as MuseInnerRecord;
|
|
143
|
+
return [inner];
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(obj.children)) {
|
|
149
|
+
const out: MuseInnerRecord[] = [];
|
|
150
|
+
for (const child of obj.children) {
|
|
151
|
+
if (
|
|
152
|
+
child &&
|
|
153
|
+
typeof child === "object" &&
|
|
154
|
+
typeof (child as { record_json?: unknown }).record_json === "string"
|
|
155
|
+
) {
|
|
156
|
+
try {
|
|
157
|
+
out.push(JSON.parse((child as { record_json: string }).record_json));
|
|
158
|
+
} catch {
|
|
159
|
+
/* skip malformed child */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
return [obj as MuseInnerRecord];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
169
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nonEmptyText(value: unknown): string | null {
|
|
173
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Collect `{ kind: "text", text }` strings from intent/model message blocks. */
|
|
177
|
+
function collectTextBlocks(value: unknown): string[] {
|
|
178
|
+
const out: string[] = [];
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
for (const block of value) {
|
|
181
|
+
if (isRecord(block) && block.kind === "text") {
|
|
182
|
+
const text = nonEmptyText(block.text);
|
|
183
|
+
if (text) out.push(text);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** User turn text from an intent payload (`refill_blocks` then `model_messages`). */
|
|
191
|
+
function intentText(payload: Record<string, unknown>): string | null {
|
|
192
|
+
const refill = collectTextBlocks(payload.refill_blocks);
|
|
193
|
+
if (refill.length > 0) return refill.join("\n");
|
|
194
|
+
const modelMessages = payload.model_messages;
|
|
195
|
+
if (Array.isArray(modelMessages)) {
|
|
196
|
+
const parts: string[] = [];
|
|
197
|
+
for (const message of modelMessages) {
|
|
198
|
+
if (isRecord(message)) parts.push(...collectTextBlocks(message.content));
|
|
199
|
+
}
|
|
200
|
+
if (parts.length > 0) return parts.join("\n");
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Assistant reply text from a `run.assistant_message_committed` event. */
|
|
206
|
+
function assistantEventText(event: Record<string, unknown>): string | null {
|
|
207
|
+
return nonEmptyText(event.text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export class MuseSessionParser {
|
|
211
|
+
private readonly tailReader = new JsonlTailReader();
|
|
212
|
+
private readonly sessionCache: IncrementalJsonlSummary<MuseSummaryState>;
|
|
213
|
+
|
|
214
|
+
constructor() {
|
|
215
|
+
this.sessionCache = new IncrementalJsonlSummary(this.summaryReducer, {});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
readSession(filePath: string, projectPath: string): MuseSession | null {
|
|
219
|
+
let content: string;
|
|
220
|
+
try {
|
|
221
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (!content.trim()) return null;
|
|
226
|
+
return this.toSession(filePath, projectPath, reduceJsonlContent(this.summaryReducer, content));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
readSessionIncremental(filePath: string, projectPath: string): MuseSession | null {
|
|
230
|
+
const result = this.sessionCache.read(filePath);
|
|
231
|
+
return result ? this.toSession(filePath, projectPath, result.state) : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pruneSessionCache(): void {
|
|
235
|
+
this.sessionCache.prune();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private readonly summaryReducer: JsonlSummaryReducer<MuseSummaryState> = {
|
|
239
|
+
initial: () => ({ seenFirstLine: false }),
|
|
240
|
+
reduce: (state, value) => {
|
|
241
|
+
const next: MuseSummaryState = { ...state, seenFirstLine: true };
|
|
242
|
+
// Session start comes from the first line only; the first user message
|
|
243
|
+
// may come from anywhere before a bounded-skip (mirrors Claude semantics).
|
|
244
|
+
const flags = { isHead: !state.seenFirstLine, canBeFirst: !state.pastHead };
|
|
245
|
+
for (const record of unwrapOuterRecords(value)) {
|
|
246
|
+
this.foldRecord(next, record, flags);
|
|
247
|
+
}
|
|
248
|
+
return next;
|
|
249
|
+
},
|
|
250
|
+
skip: (state) => ({
|
|
251
|
+
seenFirstLine: true,
|
|
252
|
+
pastHead: true,
|
|
253
|
+
sessionStartMs: state.sessionStartMs,
|
|
254
|
+
firstUserMessage: state.firstUserMessage,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Fold one inner record into the summary. `isHead` marks the first line
|
|
260
|
+
* (session start only); `canBeFirst` allows the first user message from any
|
|
261
|
+
* line read before a bounded-skip.
|
|
262
|
+
*/
|
|
263
|
+
private foldRecord(
|
|
264
|
+
next: MuseSummaryState,
|
|
265
|
+
record: MuseInnerRecord,
|
|
266
|
+
flags: { isHead: boolean; canBeFirst: boolean },
|
|
267
|
+
): void {
|
|
268
|
+
const { isHead, canBeFirst } = flags;
|
|
269
|
+
const atMs = museTimestampToMs(record.recorded_at);
|
|
270
|
+
if (atMs !== null) {
|
|
271
|
+
if (isHead && next.sessionStartMs === undefined) next.sessionStartMs = atMs;
|
|
272
|
+
next.lastActiveMs = atMs;
|
|
273
|
+
}
|
|
274
|
+
const payload = record.payload;
|
|
275
|
+
if (!isRecord(payload)) return;
|
|
276
|
+
switch (classifyRecord(record)) {
|
|
277
|
+
case "metadata": {
|
|
278
|
+
if (!isRecord(payload.record)) return;
|
|
279
|
+
const root = nonEmptyText(payload.record.workspace_root);
|
|
280
|
+
if (root) next.workspaceRoot = root;
|
|
281
|
+
const model = nonEmptyText(payload.record.model_id);
|
|
282
|
+
if (model) next.modelId = model;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
case "name-changed": {
|
|
286
|
+
const name = nonEmptyText(payload.new_name);
|
|
287
|
+
if (name) next.displayName = name;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
case "user-intent": {
|
|
291
|
+
next.lastSignal = "user-intent";
|
|
292
|
+
const text = intentText(payload);
|
|
293
|
+
if (text) {
|
|
294
|
+
next.lastUserMessage = text;
|
|
295
|
+
if (canBeFirst && !next.firstUserMessage) next.firstUserMessage = text;
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
case "assistant":
|
|
300
|
+
next.lastSignal = "assistant";
|
|
301
|
+
return;
|
|
302
|
+
case "approval-started":
|
|
303
|
+
next.lastSignal = "approval-wait";
|
|
304
|
+
return;
|
|
305
|
+
case "approval-settled":
|
|
306
|
+
if (next.lastSignal === "approval-wait") next.lastSignal = "assistant";
|
|
307
|
+
return;
|
|
308
|
+
case "input-requested":
|
|
309
|
+
next.lastSignal = "input-requested";
|
|
310
|
+
return;
|
|
311
|
+
case "input-settled":
|
|
312
|
+
if (next.lastSignal === "input-requested") next.lastSignal = "assistant";
|
|
313
|
+
return;
|
|
314
|
+
case "run-terminal":
|
|
315
|
+
next.lastSignal = "run-terminal";
|
|
316
|
+
return;
|
|
317
|
+
default:
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private toSession(filePath: string, projectPath: string, state: MuseSummaryState): MuseSession {
|
|
323
|
+
const sessionStartMs = state.sessionStartMs ?? state.lastActiveMs;
|
|
324
|
+
return {
|
|
325
|
+
sessionId: path.basename(path.dirname(filePath)),
|
|
326
|
+
projectPath: projectPath || state.workspaceRoot || "",
|
|
327
|
+
displayName: state.displayName,
|
|
328
|
+
modelId: state.modelId,
|
|
329
|
+
sessionStart: sessionStartMs !== undefined ? new Date(sessionStartMs) : new Date(),
|
|
330
|
+
lastActive: state.lastActiveMs !== undefined ? new Date(state.lastActiveMs) : new Date(),
|
|
331
|
+
lastSignal: state.lastSignal,
|
|
332
|
+
lastUserMessage: state.lastUserMessage,
|
|
333
|
+
firstUserMessage: state.firstUserMessage,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Status mapping over the last conversation signal:
|
|
339
|
+
* - user-intent → RUNNING (agent is processing the new turn)
|
|
340
|
+
* - assistant → WAITING (reply committed, awaiting user)
|
|
341
|
+
* - approval-wait / input-requested → WAITING (blocked on the user)
|
|
342
|
+
* - run-terminal → IDLE; no signal → UNKNOWN.
|
|
343
|
+
*/
|
|
344
|
+
determineStatus(session: MuseSession): AgentStatus {
|
|
345
|
+
switch (session.lastSignal) {
|
|
346
|
+
case "user-intent":
|
|
347
|
+
return AgentStatus.RUNNING;
|
|
348
|
+
case "assistant":
|
|
349
|
+
case "approval-wait":
|
|
350
|
+
case "input-requested":
|
|
351
|
+
return AgentStatus.WAITING;
|
|
352
|
+
case "run-terminal":
|
|
353
|
+
return AgentStatus.IDLE;
|
|
354
|
+
default:
|
|
355
|
+
return AgentStatus.UNKNOWN;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getConversation(sessionFilePath: string, options?: ConversationOptions): ConversationMessage[] {
|
|
360
|
+
const verbose = options?.verbose ?? false;
|
|
361
|
+
const tail = normalizeTail(options?.tail);
|
|
362
|
+
if (tail !== undefined) {
|
|
363
|
+
return this.tailReader.read(
|
|
364
|
+
sessionFilePath,
|
|
365
|
+
tail,
|
|
366
|
+
{ parseLine: (line) => this.lineToMessage(line, verbose) },
|
|
367
|
+
String(verbose),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
let content: string;
|
|
371
|
+
try {
|
|
372
|
+
content = fs.readFileSync(sessionFilePath, "utf-8");
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
const messages: ConversationMessage[] = [];
|
|
377
|
+
for (const line of content.trim().split("\n")) {
|
|
378
|
+
messages.push(...this.linesToMessages(line, verbose));
|
|
379
|
+
}
|
|
380
|
+
return messages;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** One raw frame line → conversation messages (usually zero or one). */
|
|
384
|
+
private linesToMessages(line: string, verbose: boolean): ConversationMessage[] {
|
|
385
|
+
const out: ConversationMessage[] = [];
|
|
386
|
+
for (const record of unwrapFrameRecords(line)) {
|
|
387
|
+
const message = this.recordToMessage(record, verbose);
|
|
388
|
+
if (message) out.push(message);
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* One raw frame line → its first conversation message, for tail reads.
|
|
395
|
+
* A retained frame can theoretically hold several messages; the tail
|
|
396
|
+
* window counts lines, so a boundary line keeps only its first message.
|
|
397
|
+
*/
|
|
398
|
+
private lineToMessage(line: string, verbose: boolean): ConversationMessage | null {
|
|
399
|
+
return this.linesToMessages(line, verbose)[0] ?? null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private recordToMessage(record: MuseInnerRecord, verbose: boolean): ConversationMessage | null {
|
|
403
|
+
const payload = record.payload;
|
|
404
|
+
if (!isRecord(payload)) return null;
|
|
405
|
+
const atMs = museTimestampToMs(record.recorded_at);
|
|
406
|
+
const timestamp = atMs !== null ? new Date(atMs).toISOString() : undefined;
|
|
407
|
+
|
|
408
|
+
switch (classifyRecord(record)) {
|
|
409
|
+
case "user-intent": {
|
|
410
|
+
const text = intentText(payload);
|
|
411
|
+
if (!text) return null;
|
|
412
|
+
return { role: "user", content: text, timestamp };
|
|
413
|
+
}
|
|
414
|
+
case "assistant": {
|
|
415
|
+
if (!isRecord(payload.event)) return null;
|
|
416
|
+
const text = assistantEventText(payload.event);
|
|
417
|
+
if (!text) return null;
|
|
418
|
+
return { role: "assistant", content: text, timestamp };
|
|
419
|
+
}
|
|
420
|
+
case "tool-calls": {
|
|
421
|
+
if (!verbose || !isRecord(payload.event) || !Array.isArray(payload.event.tool_calls)) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const names = (payload.event.tool_calls as unknown[])
|
|
425
|
+
.filter(isRecord)
|
|
426
|
+
.map((call) => nonEmptyText(call.tool_name))
|
|
427
|
+
.filter((name): name is string => name !== null);
|
|
428
|
+
if (names.length === 0) return null;
|
|
429
|
+
return { role: "assistant", content: `tool calls: ${names.join(", ")}`, timestamp };
|
|
430
|
+
}
|
|
431
|
+
default:
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { displayHome, record } from "../readiness/checks.js";
|
|
3
|
+
import type {
|
|
4
|
+
AuthReadinessCheck,
|
|
5
|
+
HarnessReadinessProfile,
|
|
6
|
+
ReadinessRuntime,
|
|
7
|
+
} from "../readiness/types.js";
|
|
8
|
+
|
|
9
|
+
async function authCheck(runtime: ReadinessRuntime): Promise<AuthReadinessCheck> {
|
|
10
|
+
const sourcePath = join(runtime.homeDir, ".config", "muse", "auth.json");
|
|
11
|
+
try {
|
|
12
|
+
const parsed = record(JSON.parse(await runtime.readFile(sourcePath)));
|
|
13
|
+
if (!parsed) throw new Error("invalid");
|
|
14
|
+
const authenticated = Object.keys(parsed).length > 0;
|
|
15
|
+
return {
|
|
16
|
+
state: authenticated ? "authenticated" : "unauthenticated",
|
|
17
|
+
source: displayHome(sourcePath, runtime.homeDir),
|
|
18
|
+
provider: null,
|
|
19
|
+
availableProviders: [],
|
|
20
|
+
status: authenticated ? "pass" : "fail",
|
|
21
|
+
errors: authenticated ? [] : ["Muse credential file has no entries"],
|
|
22
|
+
};
|
|
23
|
+
} catch {
|
|
24
|
+
return {
|
|
25
|
+
state: "unauthenticated",
|
|
26
|
+
source: displayHome(sourcePath, runtime.homeDir),
|
|
27
|
+
provider: null,
|
|
28
|
+
availableProviders: [],
|
|
29
|
+
status: "fail",
|
|
30
|
+
errors: ["Muse credential file is missing or invalid"],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const museReadiness: HarnessReadinessProfile = {
|
|
36
|
+
configDir: ".config/muse",
|
|
37
|
+
auth: authCheck,
|
|
38
|
+
};
|
|
@@ -4,6 +4,7 @@ import { copilotReadiness } from "../copilot/readiness.js";
|
|
|
4
4
|
import { openCodeReadiness } from "../opencode/readiness.js";
|
|
5
5
|
import { piReadiness } from "../pi/readiness.js";
|
|
6
6
|
import { devinReadiness } from "../devin/readiness.js";
|
|
7
|
+
import { museReadiness } from "../muse/readiness.js";
|
|
7
8
|
import {
|
|
8
9
|
builtInSkillsCheck,
|
|
9
10
|
createReadinessRuntime,
|
|
@@ -31,6 +32,7 @@ const READINESS_PROFILES: Record<ReadinessAgentType, HarnessReadinessProfile> =
|
|
|
31
32
|
copilot: copilotReadiness,
|
|
32
33
|
pi: piReadiness,
|
|
33
34
|
devin: devinReadiness,
|
|
35
|
+
muse: museReadiness,
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
export async function getAgentReadinessReport(
|
|
@@ -22,6 +22,7 @@ export const HARNESS_RUNTIME_PROFILES: Record<StartableAgentType, HarnessRuntime
|
|
|
22
22
|
pi: profile("pi", matchAnyBasename(["pi"])),
|
|
23
23
|
kiro: profile("kiro", matchAnyBasename(["kiro-cli", "kiro"]), ["kiro_cli", "kiro-cli"]),
|
|
24
24
|
devin: profile("devin", matchArgv0("devin")),
|
|
25
|
+
muse: profile("muse", matchArgv0Name("muse")),
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export function runtimeAgentMatchesHarness(runtimeAgent: string, type: AgentType): boolean {
|
package/src/harnesses/shared.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
captureProcessSnapshot,
|
|
17
17
|
executableBasename,
|
|
18
18
|
filterByProcessNames,
|
|
19
|
+
snapshotOptions,
|
|
19
20
|
} from "../utils/process.js";
|
|
20
21
|
|
|
21
22
|
/** A session untouched for longer than this is IDLE. */
|
|
@@ -37,15 +38,19 @@ export interface HarnessProcesses {
|
|
|
37
38
|
* (e.g. a harness's child runtime) that detection may need to walk through.
|
|
38
39
|
*/
|
|
39
40
|
export async function findHarnessProcesses(
|
|
40
|
-
adapter: Pick<AgentAdapter, "canHandle"> & {
|
|
41
|
+
adapter: Pick<AgentAdapter, "canHandle" | "processNamePrefixes"> & {
|
|
42
|
+
readonly processNames: readonly string[];
|
|
43
|
+
},
|
|
41
44
|
context?: AgentDetectionContext,
|
|
42
45
|
): Promise<HarnessProcesses> {
|
|
46
|
+
const prefixes = adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [];
|
|
43
47
|
const snapshot =
|
|
44
48
|
context?.processes ??
|
|
45
|
-
(await captureProcessSnapshot(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
(await captureProcessSnapshot(
|
|
50
|
+
adapter.processNames,
|
|
51
|
+
snapshotOptions(prefixes, (process) => adapter.canHandle(process)),
|
|
52
|
+
));
|
|
53
|
+
const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);
|
|
49
54
|
|
|
50
55
|
const byPid = new Map<number, ProcessInfo>();
|
|
51
56
|
for (const process of relevant) {
|
package/src/utils/process.ts
CHANGED
|
@@ -88,13 +88,42 @@ function matchesExecutableName(command: string, names: ReadonlySet<string>): boo
|
|
|
88
88
|
return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
|
|
93
|
+
*
|
|
94
|
+
* Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
|
|
95
|
+
* `muse`. This is a recall-oriented prefilter: adapters still apply
|
|
96
|
+
* `canHandle` for precise shaping, so a coincidental `prefix-*` binary
|
|
97
|
+
* cannot leak into another harness.
|
|
98
|
+
*/
|
|
99
|
+
export function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean {
|
|
100
|
+
if (prefixes.size === 0) return false;
|
|
101
|
+
const base = normalizeExecutableName(pathBasename(command.trim().split(/\s+/)[0] ?? ""));
|
|
102
|
+
if (prefixes.has(base)) return true;
|
|
103
|
+
for (const prefix of prefixes) {
|
|
104
|
+
if (base.startsWith(`${prefix}-`)) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */
|
|
110
|
+
export function matchesProcessPatterns(
|
|
111
|
+
command: string,
|
|
112
|
+
names: ReadonlySet<string>,
|
|
113
|
+
prefixes: ReadonlySet<string> = new Set(),
|
|
114
|
+
): boolean {
|
|
115
|
+
return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
export function filterByProcessNames(
|
|
92
119
|
processes: readonly ProcessInfo[],
|
|
93
120
|
namePatterns: readonly string[],
|
|
121
|
+
namePrefixes: readonly string[] = [],
|
|
94
122
|
): ProcessInfo[] {
|
|
95
123
|
const names = normalizedProcessNames(namePatterns);
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
const prefixes = normalizedProcessNames(namePrefixes);
|
|
125
|
+
if (names.size === 0 && prefixes.size === 0) return [];
|
|
126
|
+
return processes.filter((process) => matchesProcessPatterns(process.command, names, prefixes));
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -160,6 +189,11 @@ export interface ProcessSnapshotOptions {
|
|
|
160
189
|
* only) so adapters can walk parent chains. Omitted = enrich everything.
|
|
161
190
|
*/
|
|
162
191
|
isCandidate?: (process: ProcessInfo) => boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Basename prefixes matched like `processNamePrefixes`: exact or
|
|
194
|
+
* `<prefix>-*`. Same validation as namePatterns.
|
|
195
|
+
*/
|
|
196
|
+
namePrefixes?: readonly string[];
|
|
163
197
|
}
|
|
164
198
|
|
|
165
199
|
export type ProcessSnapshotCapture = (
|
|
@@ -167,6 +201,20 @@ export type ProcessSnapshotCapture = (
|
|
|
167
201
|
options?: ProcessSnapshotOptions,
|
|
168
202
|
) => Promise<ProcessInfo[]>;
|
|
169
203
|
|
|
204
|
+
/**
|
|
205
|
+
* One construction site for snapshot options: `namePrefixes` is sent only
|
|
206
|
+
* when non-empty, so adapters without prefixes keep the exact legacy call
|
|
207
|
+
* shape that existing mock assertions pin.
|
|
208
|
+
*/
|
|
209
|
+
export function snapshotOptions(
|
|
210
|
+
namePrefixes: readonly string[],
|
|
211
|
+
isCandidate: (process: ProcessInfo) => boolean,
|
|
212
|
+
): ProcessSnapshotOptions {
|
|
213
|
+
return namePrefixes.length > 0
|
|
214
|
+
? { namePrefixes: [...namePrefixes], isCandidate }
|
|
215
|
+
: { isCandidate };
|
|
216
|
+
}
|
|
217
|
+
|
|
170
218
|
const execFileText: ProcessExec = (file, args) =>
|
|
171
219
|
new Promise((resolve, reject) => {
|
|
172
220
|
execFile(
|
|
@@ -190,7 +238,11 @@ interface ParsedProcessList {
|
|
|
190
238
|
matched: ProcessInfo[];
|
|
191
239
|
}
|
|
192
240
|
|
|
193
|
-
function parseProcessList(
|
|
241
|
+
function parseProcessList(
|
|
242
|
+
output: string,
|
|
243
|
+
namePatterns: ReadonlySet<string>,
|
|
244
|
+
namePrefixes: ReadonlySet<string> = new Set(),
|
|
245
|
+
): ParsedProcessList {
|
|
194
246
|
const allPids = new Set<number>();
|
|
195
247
|
const matched: ProcessInfo[] = [];
|
|
196
248
|
|
|
@@ -207,7 +259,7 @@ function parseProcessList(output: string, namePatterns: ReadonlySet<string>): Pa
|
|
|
207
259
|
|
|
208
260
|
const tty = match[3];
|
|
209
261
|
const command = match[4];
|
|
210
|
-
if (!
|
|
262
|
+
if (!matchesProcessPatterns(command, namePatterns, namePrefixes)) continue;
|
|
211
263
|
|
|
212
264
|
matched.push({
|
|
213
265
|
pid,
|
|
@@ -317,14 +369,14 @@ export function createProcessSnapshotCapture(
|
|
|
317
369
|
};
|
|
318
370
|
|
|
319
371
|
return async (namePatterns, options = {}) => {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
);
|
|
323
|
-
if (names.size === 0) return [];
|
|
372
|
+
const valid = (name: string) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name);
|
|
373
|
+
const names = normalizedProcessNames(namePatterns.filter(valid));
|
|
374
|
+
const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));
|
|
375
|
+
if (names.size === 0 && prefixes.size === 0) return [];
|
|
324
376
|
|
|
325
377
|
try {
|
|
326
378
|
const output = await exec("ps", ["-axo", "pid=,ppid=,tty=,command="]);
|
|
327
|
-
const { allPids, matched } = parseProcessList(output, names);
|
|
379
|
+
const { allPids, matched } = parseProcessList(output, names, prefixes);
|
|
328
380
|
|
|
329
381
|
for (const pid of startTimeCache.keys()) {
|
|
330
382
|
if (!allPids.has(pid)) startTimeCache.delete(pid);
|