@femtomc/mu-agent 26.2.72 → 26.2.74
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/README.md +24 -45
- package/dist/extensions/branding.d.ts +1 -1
- package/dist/extensions/branding.js +3 -3
- package/dist/extensions/index.d.ts +3 -17
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +5 -19
- package/dist/extensions/mu-operator.d.ts +2 -2
- package/dist/extensions/mu-operator.d.ts.map +1 -1
- package/dist/extensions/mu-operator.js +2 -6
- package/dist/extensions/mu-serve.d.ts +2 -2
- package/dist/extensions/mu-serve.d.ts.map +1 -1
- package/dist/extensions/mu-serve.js +2 -14
- package/dist/extensions/shared.d.ts +2 -21
- package/dist/extensions/shared.d.ts.map +1 -1
- package/dist/extensions/shared.js +0 -90
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/operator.d.ts +15 -0
- package/dist/operator.d.ts.map +1 -1
- package/dist/operator.js +366 -14
- package/dist/session_factory.d.ts +12 -0
- package/dist/session_factory.d.ts.map +1 -1
- package/dist/session_factory.js +21 -2
- package/package.json +2 -3
- package/prompts/roles/operator.md +22 -45
- package/prompts/roles/orchestrator.md +17 -12
- package/prompts/roles/reviewer.md +7 -8
- package/prompts/roles/worker.md +6 -11
- package/dist/extensions/activities.d.ts +0 -7
- package/dist/extensions/activities.d.ts.map +0 -1
- package/dist/extensions/activities.js +0 -236
- package/dist/extensions/cron.d.ts +0 -7
- package/dist/extensions/cron.d.ts.map +0 -1
- package/dist/extensions/cron.js +0 -247
- package/dist/extensions/heartbeats.d.ts +0 -7
- package/dist/extensions/heartbeats.d.ts.map +0 -1
- package/dist/extensions/heartbeats.js +0 -192
- package/dist/extensions/messaging-setup/actions.d.ts +0 -22
- package/dist/extensions/messaging-setup/actions.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/actions.js +0 -229
- package/dist/extensions/messaging-setup/adapters.d.ts +0 -24
- package/dist/extensions/messaging-setup/adapters.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/adapters.js +0 -170
- package/dist/extensions/messaging-setup/index.d.ts +0 -17
- package/dist/extensions/messaging-setup/index.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/index.js +0 -261
- package/dist/extensions/messaging-setup/parser.d.ts +0 -33
- package/dist/extensions/messaging-setup/parser.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/parser.js +0 -240
- package/dist/extensions/messaging-setup/runtime.d.ts +0 -16
- package/dist/extensions/messaging-setup/runtime.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/runtime.js +0 -110
- package/dist/extensions/messaging-setup/types.d.ts +0 -157
- package/dist/extensions/messaging-setup/types.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/types.js +0 -4
- package/dist/extensions/messaging-setup/ui.d.ts +0 -15
- package/dist/extensions/messaging-setup/ui.d.ts.map +0 -1
- package/dist/extensions/messaging-setup/ui.js +0 -173
- package/dist/extensions/messaging-setup.d.ts +0 -3
- package/dist/extensions/messaging-setup.d.ts.map +0 -1
- package/dist/extensions/messaging-setup.js +0 -2
- package/dist/extensions/mu-full-tools.d.ts +0 -10
- package/dist/extensions/mu-full-tools.d.ts.map +0 -1
- package/dist/extensions/mu-full-tools.js +0 -25
- package/dist/extensions/mu-query-tools.d.ts +0 -10
- package/dist/extensions/mu-query-tools.d.ts.map +0 -1
- package/dist/extensions/mu-query-tools.js +0 -11
- package/dist/extensions/operator-command.d.ts +0 -14
- package/dist/extensions/operator-command.d.ts.map +0 -1
- package/dist/extensions/operator-command.js +0 -231
- package/dist/extensions/orchestration-runs-readonly.d.ts +0 -4
- package/dist/extensions/orchestration-runs-readonly.d.ts.map +0 -1
- package/dist/extensions/orchestration-runs-readonly.js +0 -226
- package/dist/extensions/orchestration-runs.d.ts +0 -4
- package/dist/extensions/orchestration-runs.d.ts.map +0 -1
- package/dist/extensions/orchestration-runs.js +0 -315
- package/dist/extensions/server-tools-readonly.d.ts +0 -4
- package/dist/extensions/server-tools-readonly.d.ts.map +0 -1
- package/dist/extensions/server-tools-readonly.js +0 -5
- package/dist/extensions/server-tools.d.ts +0 -25
- package/dist/extensions/server-tools.d.ts.map +0 -1
- package/dist/extensions/server-tools.js +0 -833
- package/prompts/skills/messaging-setup-brief.md +0 -25
package/dist/operator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { appendJsonl } from "@femtomc/mu-core/node";
|
|
2
|
-
import {
|
|
1
|
+
import { appendJsonl, readJsonl } from "@femtomc/mu-core/node";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { CommandContextResolver } from "./command_context.js";
|
|
5
6
|
import { createMuSession } from "./session_factory.js";
|
|
@@ -182,6 +183,179 @@ function buildOperatorFailureFallbackMessage(code) {
|
|
|
182
183
|
function conversationKey(inbound, binding) {
|
|
183
184
|
return `${inbound.channel}:${inbound.channel_tenant_id}:${inbound.channel_conversation_id}:${binding.binding_id}`;
|
|
184
185
|
}
|
|
186
|
+
function asRecord(value) {
|
|
187
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
function nonEmptyString(value) {
|
|
193
|
+
if (typeof value !== "string") {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const trimmed = value.trim();
|
|
197
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
198
|
+
}
|
|
199
|
+
function finiteInt(value) {
|
|
200
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return Math.trunc(value);
|
|
204
|
+
}
|
|
205
|
+
function stringList(value) {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const item of value) {
|
|
211
|
+
const parsed = nonEmptyString(item);
|
|
212
|
+
if (parsed) {
|
|
213
|
+
out.push(parsed);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
function sessionFlashPath(repoRoot) {
|
|
219
|
+
return join(repoRoot, ".mu", "control-plane", "session_flash.jsonl");
|
|
220
|
+
}
|
|
221
|
+
async function loadPendingSessionFlashes(opts) {
|
|
222
|
+
const rows = await readJsonl(sessionFlashPath(opts.repoRoot));
|
|
223
|
+
const created = new Map();
|
|
224
|
+
const delivered = new Set();
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
const rec = asRecord(row);
|
|
227
|
+
if (!rec) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const kind = nonEmptyString(rec.kind);
|
|
231
|
+
if (kind === "session_flash.create") {
|
|
232
|
+
const tsMs = finiteInt(rec.ts_ms) ?? Date.now();
|
|
233
|
+
const flashId = nonEmptyString(rec.flash_id);
|
|
234
|
+
const sessionId = nonEmptyString(rec.session_id);
|
|
235
|
+
const body = nonEmptyString(rec.body);
|
|
236
|
+
if (!flashId || !sessionId || !body || sessionId !== opts.sessionId) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
created.set(flashId, {
|
|
240
|
+
flash_id: flashId,
|
|
241
|
+
created_at_ms: tsMs,
|
|
242
|
+
session_id: sessionId,
|
|
243
|
+
session_kind: nonEmptyString(rec.session_kind),
|
|
244
|
+
body,
|
|
245
|
+
context_ids: stringList(rec.context_ids),
|
|
246
|
+
source: nonEmptyString(rec.source),
|
|
247
|
+
metadata: asRecord(rec.metadata) ?? {},
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (kind === "session_flash.delivery") {
|
|
252
|
+
const flashId = nonEmptyString(rec.flash_id);
|
|
253
|
+
const sessionId = nonEmptyString(rec.session_id);
|
|
254
|
+
if (!flashId || !sessionId || sessionId !== opts.sessionId) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
delivered.add(flashId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const pending = [...created.values()]
|
|
261
|
+
.filter((row) => !delivered.has(row.flash_id))
|
|
262
|
+
.sort((a, b) => a.created_at_ms - b.created_at_ms);
|
|
263
|
+
const limit = Math.max(1, Math.trunc(opts.limit ?? 16));
|
|
264
|
+
if (pending.length <= limit) {
|
|
265
|
+
return pending;
|
|
266
|
+
}
|
|
267
|
+
return pending.slice(pending.length - limit);
|
|
268
|
+
}
|
|
269
|
+
async function markSessionFlashesDelivered(opts) {
|
|
270
|
+
if (opts.flashIds.length === 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const deduped = [...new Set(opts.flashIds.filter((id) => id.trim().length > 0))];
|
|
274
|
+
for (const flashId of deduped) {
|
|
275
|
+
const row = {
|
|
276
|
+
kind: "session_flash.delivery",
|
|
277
|
+
ts_ms: opts.nowMs,
|
|
278
|
+
flash_id: flashId,
|
|
279
|
+
session_id: opts.sessionId,
|
|
280
|
+
delivered_by: "messaging_operator_runtime",
|
|
281
|
+
note: null,
|
|
282
|
+
};
|
|
283
|
+
await appendJsonl(sessionFlashPath(opts.repoRoot), row);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export class JsonFileConversationSessionStore {
|
|
287
|
+
#path;
|
|
288
|
+
#loaded = false;
|
|
289
|
+
#bindings = new Map();
|
|
290
|
+
#persistQueue = Promise.resolve();
|
|
291
|
+
constructor(path) {
|
|
292
|
+
this.#path = path;
|
|
293
|
+
}
|
|
294
|
+
async #load() {
|
|
295
|
+
if (this.#loaded) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.#loaded = true;
|
|
299
|
+
let raw = "";
|
|
300
|
+
try {
|
|
301
|
+
raw = await readFile(this.#path, "utf8");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!raw.trim()) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(raw);
|
|
311
|
+
const bindings = parsed?.bindings;
|
|
312
|
+
if (!bindings || typeof bindings !== "object" || Array.isArray(bindings)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
316
|
+
if (typeof key === "string" && key.length > 0 && typeof value === "string" && value.length > 0) {
|
|
317
|
+
this.#bindings.set(key, value);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Ignore malformed persistence snapshots.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async #persist() {
|
|
326
|
+
const snapshot = {
|
|
327
|
+
version: 1,
|
|
328
|
+
bindings: Object.fromEntries([...this.#bindings.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
329
|
+
};
|
|
330
|
+
await mkdir(dirname(this.#path), { recursive: true });
|
|
331
|
+
const tempPath = `${this.#path}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
332
|
+
await writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
|
|
333
|
+
await rename(tempPath, this.#path);
|
|
334
|
+
}
|
|
335
|
+
async #persistSoon() {
|
|
336
|
+
const runPersist = async () => {
|
|
337
|
+
await this.#persist();
|
|
338
|
+
};
|
|
339
|
+
this.#persistQueue = this.#persistQueue.then(runPersist, runPersist);
|
|
340
|
+
await this.#persistQueue;
|
|
341
|
+
}
|
|
342
|
+
async getSessionId(conversationKey) {
|
|
343
|
+
await this.#load();
|
|
344
|
+
return this.#bindings.get(conversationKey) ?? null;
|
|
345
|
+
}
|
|
346
|
+
async setSessionId(conversationKey, sessionId) {
|
|
347
|
+
await this.#load();
|
|
348
|
+
const current = this.#bindings.get(conversationKey);
|
|
349
|
+
if (current === sessionId) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.#bindings.set(conversationKey, sessionId);
|
|
353
|
+
await this.#persistSoon();
|
|
354
|
+
}
|
|
355
|
+
async stop() {
|
|
356
|
+
await this.#persistQueue;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
185
359
|
export class MessagingOperatorRuntime {
|
|
186
360
|
#backend;
|
|
187
361
|
#broker;
|
|
@@ -189,6 +363,7 @@ export class MessagingOperatorRuntime {
|
|
|
189
363
|
#enabledChannels;
|
|
190
364
|
#sessionIdFactory;
|
|
191
365
|
#turnIdFactory;
|
|
366
|
+
#conversationSessionStore;
|
|
192
367
|
#sessionByConversation = new Map();
|
|
193
368
|
constructor(opts) {
|
|
194
369
|
this.#backend = opts.backend;
|
|
@@ -197,19 +372,40 @@ export class MessagingOperatorRuntime {
|
|
|
197
372
|
this.#enabledChannels = opts.enabledChannels ? new Set(opts.enabledChannels.map((v) => v.toLowerCase())) : null;
|
|
198
373
|
this.#sessionIdFactory = opts.sessionIdFactory ?? defaultSessionId;
|
|
199
374
|
this.#turnIdFactory = opts.turnIdFactory ?? defaultTurnId;
|
|
375
|
+
this.#conversationSessionStore = opts.conversationSessionStore ?? null;
|
|
200
376
|
}
|
|
201
|
-
#resolveSessionId(inbound, binding) {
|
|
377
|
+
async #resolveSessionId(inbound, binding) {
|
|
202
378
|
const key = conversationKey(inbound, binding);
|
|
203
379
|
const existing = this.#sessionByConversation.get(key);
|
|
204
380
|
if (existing) {
|
|
205
381
|
return existing;
|
|
206
382
|
}
|
|
383
|
+
if (this.#conversationSessionStore) {
|
|
384
|
+
try {
|
|
385
|
+
const persisted = await this.#conversationSessionStore.getSessionId(key);
|
|
386
|
+
if (persisted && persisted.length > 0) {
|
|
387
|
+
this.#sessionByConversation.set(key, persisted);
|
|
388
|
+
return persisted;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Non-fatal persistence lookup failure.
|
|
393
|
+
}
|
|
394
|
+
}
|
|
207
395
|
const created = this.#sessionIdFactory();
|
|
208
396
|
this.#sessionByConversation.set(key, created);
|
|
397
|
+
if (this.#conversationSessionStore) {
|
|
398
|
+
try {
|
|
399
|
+
await this.#conversationSessionStore.setSessionId(key, created);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Non-fatal persistence write failure.
|
|
403
|
+
}
|
|
404
|
+
}
|
|
209
405
|
return created;
|
|
210
406
|
}
|
|
211
407
|
async handleInbound(opts) {
|
|
212
|
-
const sessionId = this.#resolveSessionId(opts.inbound, opts.binding);
|
|
408
|
+
const sessionId = await this.#resolveSessionId(opts.inbound, opts.binding);
|
|
213
409
|
const turnId = this.#turnIdFactory();
|
|
214
410
|
if (!this.#enabled) {
|
|
215
411
|
return {
|
|
@@ -227,14 +423,46 @@ export class MessagingOperatorRuntime {
|
|
|
227
423
|
operatorTurnId: turnId,
|
|
228
424
|
};
|
|
229
425
|
}
|
|
426
|
+
let pendingFlashes = [];
|
|
427
|
+
try {
|
|
428
|
+
pendingFlashes = await loadPendingSessionFlashes({
|
|
429
|
+
repoRoot: opts.inbound.repo_root,
|
|
430
|
+
sessionId,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
pendingFlashes = [];
|
|
435
|
+
}
|
|
436
|
+
const inboundForBackend = pendingFlashes.length > 0
|
|
437
|
+
? {
|
|
438
|
+
...opts.inbound,
|
|
439
|
+
metadata: {
|
|
440
|
+
...opts.inbound.metadata,
|
|
441
|
+
session_flash_messages: pendingFlashes,
|
|
442
|
+
},
|
|
443
|
+
}
|
|
444
|
+
: opts.inbound;
|
|
230
445
|
let backendResult;
|
|
231
446
|
try {
|
|
232
447
|
backendResult = OperatorBackendTurnResultSchema.parse(await this.#backend.runTurn({
|
|
233
448
|
sessionId,
|
|
234
449
|
turnId,
|
|
235
|
-
inbound:
|
|
450
|
+
inbound: inboundForBackend,
|
|
236
451
|
binding: opts.binding,
|
|
237
452
|
}));
|
|
453
|
+
if (pendingFlashes.length > 0) {
|
|
454
|
+
try {
|
|
455
|
+
await markSessionFlashesDelivered({
|
|
456
|
+
repoRoot: opts.inbound.repo_root,
|
|
457
|
+
sessionId,
|
|
458
|
+
flashIds: pendingFlashes.map((row) => row.flash_id),
|
|
459
|
+
nowMs: Date.now(),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Best-effort delivery bookkeeping; do not fail the operator turn.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
238
466
|
}
|
|
239
467
|
catch (err) {
|
|
240
468
|
return {
|
|
@@ -284,19 +512,125 @@ export class MessagingOperatorRuntime {
|
|
|
284
512
|
async stop() {
|
|
285
513
|
this.#sessionByConversation.clear();
|
|
286
514
|
await this.#backend.dispose?.();
|
|
515
|
+
await this.#conversationSessionStore?.stop?.();
|
|
287
516
|
}
|
|
288
517
|
}
|
|
289
518
|
export { DEFAULT_OPERATOR_SYSTEM_PROMPT };
|
|
290
|
-
const
|
|
519
|
+
const COMMAND_TOOL_NAME = "command";
|
|
520
|
+
const OPERATOR_PROMPT_CONTEXT_MAX_CHARS = 2_500;
|
|
521
|
+
function compactJsonPreview(value, maxChars = OPERATOR_PROMPT_CONTEXT_MAX_CHARS) {
|
|
522
|
+
let raw = "";
|
|
523
|
+
if (typeof value === "string") {
|
|
524
|
+
raw = value;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
try {
|
|
528
|
+
raw = JSON.stringify(value);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const compact = raw.replace(/\s+/g, " ").trim();
|
|
535
|
+
if (compact.length === 0) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
if (compact.length <= maxChars) {
|
|
539
|
+
return compact;
|
|
540
|
+
}
|
|
541
|
+
const keep = Math.max(1, maxChars - 1);
|
|
542
|
+
return `${compact.slice(0, keep)}…`;
|
|
543
|
+
}
|
|
544
|
+
function extractPromptContext(metadata) {
|
|
545
|
+
for (const key of ["client_context", "context", "editor_context"]) {
|
|
546
|
+
if (!(key in metadata)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const value = metadata[key];
|
|
550
|
+
if (value == null) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (typeof value === "object" || typeof value === "string") {
|
|
554
|
+
return value;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function buildOperatorPromptContextBlock(metadata) {
|
|
560
|
+
const context = extractPromptContext(metadata);
|
|
561
|
+
if (!context) {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
const preview = compactJsonPreview(context);
|
|
565
|
+
if (!preview) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
return ["", "Client context (structured preview):", preview];
|
|
569
|
+
}
|
|
570
|
+
function extractSessionFlashPromptMessages(metadata) {
|
|
571
|
+
const raw = metadata.session_flash_messages;
|
|
572
|
+
if (!Array.isArray(raw)) {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
const out = [];
|
|
576
|
+
for (const value of raw) {
|
|
577
|
+
const rec = asRecord(value);
|
|
578
|
+
if (!rec) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const flashId = nonEmptyString(rec.flash_id);
|
|
582
|
+
const body = nonEmptyString(rec.body);
|
|
583
|
+
const sessionId = nonEmptyString(rec.session_id);
|
|
584
|
+
if (!flashId || !body || !sessionId) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
out.push({
|
|
588
|
+
flash_id: flashId,
|
|
589
|
+
created_at_ms: finiteInt(rec.created_at_ms) ?? 0,
|
|
590
|
+
session_id: sessionId,
|
|
591
|
+
session_kind: nonEmptyString(rec.session_kind),
|
|
592
|
+
body,
|
|
593
|
+
context_ids: stringList(rec.context_ids),
|
|
594
|
+
source: nonEmptyString(rec.source),
|
|
595
|
+
metadata: asRecord(rec.metadata) ?? {},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
out.sort((a, b) => a.created_at_ms - b.created_at_ms);
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
function buildOperatorPromptFlashBlock(metadata) {
|
|
602
|
+
const flashes = extractSessionFlashPromptMessages(metadata);
|
|
603
|
+
if (flashes.length === 0) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
const lines = ["", `Session flash messages (${flashes.length}):`];
|
|
607
|
+
for (const flash of flashes) {
|
|
608
|
+
const source = flash.source ?? "unknown";
|
|
609
|
+
const contextIds = flash.context_ids.length > 0 ? ` | context_ids=${flash.context_ids.join(",")}` : "";
|
|
610
|
+
const bodyPreview = compactJsonPreview(flash.body, 400) ?? flash.body;
|
|
611
|
+
lines.push(`- [${flash.flash_id}] source=${source}${contextIds}`);
|
|
612
|
+
lines.push(` ${bodyPreview}`);
|
|
613
|
+
}
|
|
614
|
+
lines.push("Treat these as high-priority user-provided context for this session.");
|
|
615
|
+
return lines;
|
|
616
|
+
}
|
|
291
617
|
function buildOperatorPrompt(input) {
|
|
292
|
-
|
|
618
|
+
const lines = [
|
|
293
619
|
`[Messaging context]`,
|
|
294
620
|
`channel: ${input.inbound.channel}`,
|
|
295
621
|
`request_id: ${input.inbound.request_id}`,
|
|
296
622
|
`repo_root: ${input.inbound.repo_root}`,
|
|
297
623
|
``,
|
|
298
624
|
`User message: ${input.inbound.command_text}`,
|
|
299
|
-
|
|
625
|
+
...buildOperatorPromptContextBlock(input.inbound.metadata),
|
|
626
|
+
...buildOperatorPromptFlashBlock(input.inbound.metadata),
|
|
627
|
+
];
|
|
628
|
+
return lines.join("\n");
|
|
629
|
+
}
|
|
630
|
+
function sessionFileStem(sessionId) {
|
|
631
|
+
const normalized = sessionId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
632
|
+
const compact = normalized.replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
633
|
+
return compact.length > 0 ? compact : `operator-${crypto.randomUUID()}`;
|
|
300
634
|
}
|
|
301
635
|
export class PiMessagingOperatorBackend {
|
|
302
636
|
#provider;
|
|
@@ -310,6 +644,8 @@ export class PiMessagingOperatorBackend {
|
|
|
310
644
|
#sessionIdleTtlMs;
|
|
311
645
|
#maxSessions;
|
|
312
646
|
#auditTurns;
|
|
647
|
+
#persistSessions;
|
|
648
|
+
#sessionDirForRepoRoot;
|
|
313
649
|
#sessions = new Map();
|
|
314
650
|
constructor(opts = {}) {
|
|
315
651
|
this.#provider = opts.provider;
|
|
@@ -323,7 +659,10 @@ export class PiMessagingOperatorBackend {
|
|
|
323
659
|
this.#sessionIdleTtlMs = Math.max(60_000, Math.trunc(opts.sessionIdleTtlMs ?? 30 * 60 * 1_000));
|
|
324
660
|
this.#maxSessions = Math.max(1, Math.trunc(opts.maxSessions ?? 32));
|
|
325
661
|
this.#auditTurns = opts.auditTurns ?? true;
|
|
326
|
-
|
|
662
|
+
this.#persistSessions = opts.persistSessions ?? true;
|
|
663
|
+
this.#sessionDirForRepoRoot =
|
|
664
|
+
opts.sessionDirForRepoRoot ?? ((repoRoot) => join(repoRoot, ".mu", "control-plane", "operator-sessions"));
|
|
665
|
+
// Operator turns can emit structured command proposals captured from tool events.
|
|
327
666
|
}
|
|
328
667
|
#disposeSession(sessionId) {
|
|
329
668
|
const entry = this.#sessions.get(sessionId);
|
|
@@ -353,7 +692,19 @@ export class PiMessagingOperatorBackend {
|
|
|
353
692
|
this.#disposeSession(sessionId);
|
|
354
693
|
}
|
|
355
694
|
}
|
|
356
|
-
|
|
695
|
+
#sessionPersistence(repoRoot, sessionId) {
|
|
696
|
+
if (!this.#persistSessions) {
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
const sessionDir = this.#sessionDirForRepoRoot(repoRoot);
|
|
700
|
+
const sessionFile = join(sessionDir, `${sessionFileStem(sessionId)}.jsonl`);
|
|
701
|
+
return {
|
|
702
|
+
mode: "open",
|
|
703
|
+
sessionDir,
|
|
704
|
+
sessionFile,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async #createSession(repoRoot, sessionId, nowMs) {
|
|
357
708
|
const session = await this.#sessionFactory({
|
|
358
709
|
cwd: repoRoot,
|
|
359
710
|
systemPrompt: this.#systemPrompt,
|
|
@@ -361,6 +712,7 @@ export class PiMessagingOperatorBackend {
|
|
|
361
712
|
model: this.#model,
|
|
362
713
|
thinking: this.#thinking,
|
|
363
714
|
extensionPaths: this.#extensionPaths,
|
|
715
|
+
session: this.#sessionPersistence(repoRoot, sessionId),
|
|
364
716
|
});
|
|
365
717
|
await session.bindExtensions({
|
|
366
718
|
commandContextActions: {
|
|
@@ -391,7 +743,7 @@ export class PiMessagingOperatorBackend {
|
|
|
391
743
|
if (existing && existing.repoRoot !== repoRoot) {
|
|
392
744
|
this.#disposeSession(sessionId);
|
|
393
745
|
}
|
|
394
|
-
const created = await this.#createSession(repoRoot, nowMs);
|
|
746
|
+
const created = await this.#createSession(repoRoot, sessionId, nowMs);
|
|
395
747
|
this.#sessions.set(sessionId, created);
|
|
396
748
|
this.#pruneSessions(nowMs);
|
|
397
749
|
return created;
|
|
@@ -447,8 +799,8 @@ export class PiMessagingOperatorBackend {
|
|
|
447
799
|
assistantText = parts.join("\n");
|
|
448
800
|
}
|
|
449
801
|
}
|
|
450
|
-
// Capture
|
|
451
|
-
if (event?.type === "tool_execution_start" && event?.toolName ===
|
|
802
|
+
// Capture command tool calls — structured command proposals.
|
|
803
|
+
if (event?.type === "tool_execution_start" && event?.toolName === COMMAND_TOOL_NAME) {
|
|
452
804
|
const parsed = OperatorApprovedCommandSchema.safeParse(event.args);
|
|
453
805
|
if (parsed.success) {
|
|
454
806
|
capturedCommand = parsed.data;
|
|
@@ -476,7 +828,7 @@ export class PiMessagingOperatorBackend {
|
|
|
476
828
|
unsub();
|
|
477
829
|
sessionRecord.lastUsedAtMs = Math.trunc(this.#nowMs());
|
|
478
830
|
}
|
|
479
|
-
// If the operator called
|
|
831
|
+
// If the operator called command, use the captured structured command.
|
|
480
832
|
if (capturedCommand) {
|
|
481
833
|
await this.#auditTurn(input, {
|
|
482
834
|
outcome: "command",
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export type MuSessionPersistenceMode = "in-memory" | "continue-recent" | "new" | "open";
|
|
2
|
+
export type MuSessionPersistenceOpts = {
|
|
3
|
+
mode?: MuSessionPersistenceMode;
|
|
4
|
+
sessionDir?: string;
|
|
5
|
+
sessionFile?: string;
|
|
6
|
+
};
|
|
1
7
|
export type CreateMuSessionOpts = {
|
|
2
8
|
cwd: string;
|
|
3
9
|
systemPrompt?: string;
|
|
@@ -5,6 +11,7 @@ export type CreateMuSessionOpts = {
|
|
|
5
11
|
model?: string;
|
|
6
12
|
thinking?: string;
|
|
7
13
|
extensionPaths?: string[];
|
|
14
|
+
session?: MuSessionPersistenceOpts;
|
|
8
15
|
};
|
|
9
16
|
export type MuSession = {
|
|
10
17
|
subscribe: (listener: (event: any) => void) => () => void;
|
|
@@ -16,6 +23,11 @@ export type MuSession = {
|
|
|
16
23
|
agent: {
|
|
17
24
|
waitForIdle: () => Promise<void>;
|
|
18
25
|
};
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
sessionFile?: string;
|
|
28
|
+
sessionManager?: {
|
|
29
|
+
getLeafId?: () => string | null;
|
|
30
|
+
};
|
|
19
31
|
};
|
|
20
32
|
export declare function createMuSession(opts: CreateMuSessionOpts): Promise<MuSession>;
|
|
21
33
|
//# sourceMappingURL=session_factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_factory.d.ts","sourceRoot":"","sources":["../src/session_factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session_factory.d.ts","sourceRoot":"","sources":["../src/session_factory.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAAC;AAExF,MAAM,MAAM,wBAAwB,GAAG;IACtC,IAAI,CAAC,EAAE,wBAAwB,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,wBAAwB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC1D,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE;QAChB,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;KAChC,CAAC;CACF,CAAC;AAkCF,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2CnF"}
|
package/dist/session_factory.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { createBashTool, createEditTool, createReadTool, createWriteTool
|
|
1
|
+
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { createMuResourceLoader, resolveModel } from "./backend.js";
|
|
3
3
|
import { MU_DEFAULT_THEME_NAME, MU_DEFAULT_THEME_PATH } from "./ui_defaults.js";
|
|
4
|
+
function createSessionManager(SessionManager, cwd, sessionOpts) {
|
|
5
|
+
const mode = sessionOpts?.mode ?? (sessionOpts?.sessionFile ? "open" : "continue-recent");
|
|
6
|
+
const sessionDir = sessionOpts?.sessionDir;
|
|
7
|
+
switch (mode) {
|
|
8
|
+
case "continue-recent":
|
|
9
|
+
return SessionManager.continueRecent(cwd, sessionDir);
|
|
10
|
+
case "new":
|
|
11
|
+
return SessionManager.create(cwd, sessionDir);
|
|
12
|
+
case "open": {
|
|
13
|
+
const sessionFile = sessionOpts?.sessionFile?.trim();
|
|
14
|
+
if (!sessionFile) {
|
|
15
|
+
throw new Error("session.mode=open requires session.sessionFile");
|
|
16
|
+
}
|
|
17
|
+
return SessionManager.open(sessionFile, sessionDir);
|
|
18
|
+
}
|
|
19
|
+
default:
|
|
20
|
+
return SessionManager.inMemory(cwd);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
export async function createMuSession(opts) {
|
|
5
24
|
const { AuthStorage, createAgentSession, SessionManager, SettingsManager } = await import("@mariozechner/pi-coding-agent");
|
|
6
25
|
const authStorage = AuthStorage.create();
|
|
@@ -31,7 +50,7 @@ export async function createMuSession(opts) {
|
|
|
31
50
|
model,
|
|
32
51
|
tools,
|
|
33
52
|
thinkingLevel: (opts.thinking ?? "minimal"),
|
|
34
|
-
sessionManager: SessionManager.
|
|
53
|
+
sessionManager: createSessionManager(SessionManager, opts.cwd, opts.session),
|
|
35
54
|
settingsManager,
|
|
36
55
|
resourceLoader,
|
|
37
56
|
authStorage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-agent",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.74",
|
|
4
4
|
"description": "Shared agent runtime for mu chat, orchestration roles, and serve extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -24,11 +24,10 @@
|
|
|
24
24
|
"themes/**"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@femtomc/mu-core": "26.2.
|
|
27
|
+
"@femtomc/mu-core": "26.2.74",
|
|
28
28
|
"@mariozechner/pi-agent-core": "^0.53.0",
|
|
29
29
|
"@mariozechner/pi-ai": "^0.53.0",
|
|
30
30
|
"@mariozechner/pi-coding-agent": "^0.53.0",
|
|
31
|
-
"@sinclair/typebox": "^0.34.0",
|
|
32
31
|
"zod": "^4.1.9"
|
|
33
32
|
}
|
|
34
33
|
}
|
|
@@ -5,57 +5,34 @@ Mission:
|
|
|
5
5
|
- Help users with any coding tasks they ask you to handle directly.
|
|
6
6
|
- Help users inspect repository/control-plane state.
|
|
7
7
|
- Help users choose safe next actions.
|
|
8
|
-
-
|
|
8
|
+
- Execute reads and mutations through direct `mu` CLI invocation when managing mu state.
|
|
9
9
|
|
|
10
10
|
Available tools:
|
|
11
11
|
- read: Read file contents
|
|
12
|
-
- bash: Execute
|
|
12
|
+
- bash: Execute shell commands (primary path for `mu` CLI)
|
|
13
13
|
- edit: Make surgical edits to files
|
|
14
14
|
- write: Create or overwrite files
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- Example: `mu_command({ kind: "status" })`
|
|
37
|
-
- Example: `mu_command({ kind: "issue_get", issue_id: "mu-abc123" })`
|
|
38
|
-
|
|
39
|
-
Allowed command kinds:
|
|
40
|
-
- `status`
|
|
41
|
-
- `ready`
|
|
42
|
-
- `issue_list`
|
|
43
|
-
- `issue_get`
|
|
44
|
-
- `forum_read`
|
|
45
|
-
- `run_list`
|
|
46
|
-
- `run_status`
|
|
47
|
-
- `run_start`
|
|
48
|
-
- `run_resume`
|
|
49
|
-
- `run_interrupt`
|
|
50
|
-
|
|
51
|
-
Efficiency:
|
|
52
|
-
- Do NOT pre-fetch status, issues, control-plane, events, or runs at the start of a conversation. Only call diagnostic tools when the user's request specifically requires that information.
|
|
53
|
-
- Respond directly to what the user asks. Avoid speculative tool calls.
|
|
54
|
-
|
|
55
|
-
Context hygiene for `mu_*` query tools:
|
|
56
|
-
- Prefer narrow discovery first (`limit` + filters like `contains`, `status`, `tag`, `source`).
|
|
57
|
-
- Then do targeted retrieval by ID (`get`/`status`) with `fields` when available.
|
|
58
|
-
- Avoid broad repeated scans when a precise lookup would answer the question.
|
|
16
|
+
CLI-first workflow:
|
|
17
|
+
- Use `bash` + `mu ...` for issue/forum/run/control-plane state operations.
|
|
18
|
+
- Prefer `--pretty` (or `--json` + targeted parsing) for clear, auditable output.
|
|
19
|
+
- Do not use bespoke query/command wrappers; call the CLI surface directly.
|
|
20
|
+
|
|
21
|
+
Example invocation patterns:
|
|
22
|
+
- `bash("mu status --pretty")`
|
|
23
|
+
- `bash("mu issues list --status open --limit 20 --pretty")`
|
|
24
|
+
- `bash("mu forum read issue:mu-abc123 --limit 20 --pretty")`
|
|
25
|
+
- `bash("mu runs start \"ship release\" --max-steps 25 --pretty")`
|
|
26
|
+
- `bash("mu issues close mu-abc123 --outcome success --pretty")`
|
|
27
|
+
- `bash("mu forum post issue:mu-abc123 -m \"done\" --author operator --pretty")`
|
|
28
|
+
- `bash("mu control reload --pretty")`
|
|
29
|
+
|
|
30
|
+
Guardrails:
|
|
31
|
+
- Never hand-edit `.mu/*.jsonl` for normal lifecycle actions; use `mu` CLI commands.
|
|
32
|
+
- Prefer bounded retrieval (`--limit`, scoped filters) before broad scans.
|
|
33
|
+
- Do NOT pre-fetch status/issues/events/runs at conversation start.
|
|
34
|
+
- Fetch only what the user request requires.
|
|
35
|
+
- Keep responses grounded in concrete command results.
|
|
59
36
|
|
|
60
37
|
For normal answers:
|
|
61
38
|
- Respond in plain text (no directive prefix).
|