@femtomc/mu-agent 26.2.41 → 26.2.42

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.
@@ -1 +1 @@
1
- {"version":3,"file":"command_context.d.ts","sourceRoot":"","sources":["../src/command_context.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,cAAc,GAAG,MAAM,CAAC;AAE3E,MAAM,MAAM,8BAA8B,GACvC,iBAAiB,GACjB,mBAAmB,GACnB,sBAAsB,GACtB,uBAAuB,CAAC;AAE3B,MAAM,MAAM,yBAAyB,GAClC;IACA,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;CAC/B,GACD;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,8BAA8B,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEL,MAAM,MAAM,0BAA0B,GAAG;IACxC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CAAC;AAsJF,qBAAa,sBAAsB;;gBAGf,IAAI,GAAE,0BAA+B;IAQjD,OAAO,CAAC,IAAI,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;QACxB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,yBAAyB;CAqG7B"}
1
+ {"version":3,"file":"command_context.d.ts","sourceRoot":"","sources":["../src/command_context.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,cAAc,GAAG,MAAM,CAAC;AAE3E,MAAM,MAAM,8BAA8B,GACvC,iBAAiB,GACjB,mBAAmB,GACnB,sBAAsB,GACtB,uBAAuB,CAAC;AAE3B,MAAM,MAAM,yBAAyB,GAClC;IACA,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;CAC/B,GACD;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,8BAA8B,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEL,MAAM,MAAM,0BAA0B,GAAG;IACxC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CAAC;AAwJF,qBAAa,sBAAsB;;gBAGf,IAAI,GAAE,0BAA+B;IAQjD,OAAO,CAAC,IAAI,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;QACxB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,yBAAyB;CAqG7B"}
@@ -8,6 +8,8 @@ const ISSUE_TARGET_COMMANDS = new Set([
8
8
  "issue claim",
9
9
  "issue close",
10
10
  "run resume",
11
+ "run status",
12
+ "run interrupt",
11
13
  ]);
12
14
  const TOPIC_TARGET_COMMANDS = new Set(["forum read", "forum post"]);
13
15
  const GENERIC_TARGET_COMMANDS = new Set(["audit get", "dlq inspect", "dlq replay"]);
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function activitiesExtension(pi: ExtensionAPI): void;
3
+ export default activitiesExtension;
4
+ //# sourceMappingURL=activities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activities.d.ts","sourceRoot":"","sources":["../../src/extensions/activities.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAUlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAmHnD;AAED,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,125 @@
1
+ import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { clampInt, fetchMuJson, textResult, toJsonText } from "./shared.js";
4
+ function trimOrNull(value) {
5
+ if (value == null)
6
+ return null;
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : null;
9
+ }
10
+ export function activitiesExtension(pi) {
11
+ const ActivitiesParams = Type.Object({
12
+ action: StringEnum(["list", "get", "start", "progress", "heartbeat", "complete", "fail", "cancel", "events"]),
13
+ activity_id: Type.Optional(Type.String({ description: "Activity ID" })),
14
+ title: Type.Optional(Type.String({ description: "Title for start" })),
15
+ kind: Type.Optional(Type.String({ description: "Activity kind for start/list filtering" })),
16
+ heartbeat_every_ms: Type.Optional(Type.Number({ description: "Heartbeat interval in ms for start" })),
17
+ status: Type.Optional(Type.String({ description: "Status filter for list" })),
18
+ message: Type.Optional(Type.String({ description: "Progress/final message" })),
19
+ reason: Type.Optional(Type.String({ description: "Heartbeat reason" })),
20
+ limit: Type.Optional(Type.Number({ description: "Optional limit for list/events" })),
21
+ });
22
+ pi.registerTool({
23
+ name: "mu_activities",
24
+ label: "Activities",
25
+ description: "Manage generic long-running activities. Actions: list, get, start, progress, heartbeat, complete, fail, cancel, events.",
26
+ parameters: ActivitiesParams,
27
+ async execute(_toolCallId, params) {
28
+ switch (params.action) {
29
+ case "list": {
30
+ const query = new URLSearchParams();
31
+ const status = trimOrNull(params.status);
32
+ const kind = trimOrNull(params.kind);
33
+ const limit = clampInt(params.limit, 50, 1, 500);
34
+ if (status)
35
+ query.set("status", status);
36
+ if (kind)
37
+ query.set("kind", kind);
38
+ query.set("limit", String(limit));
39
+ const payload = await fetchMuJson(`/api/activities?${query.toString()}`);
40
+ return textResult(toJsonText(payload), { action: "list", status, kind, limit });
41
+ }
42
+ case "get": {
43
+ const activityId = trimOrNull(params.activity_id);
44
+ if (!activityId)
45
+ return textResult("get requires activity_id");
46
+ const payload = await fetchMuJson(`/api/activities/${encodeURIComponent(activityId)}`);
47
+ return textResult(toJsonText(payload), { action: "get", activityId });
48
+ }
49
+ case "events": {
50
+ const activityId = trimOrNull(params.activity_id);
51
+ if (!activityId)
52
+ return textResult("events requires activity_id");
53
+ const limit = clampInt(params.limit, 200, 1, 2_000);
54
+ const payload = await fetchMuJson(`/api/activities/${encodeURIComponent(activityId)}/events?limit=${limit}`);
55
+ return textResult(toJsonText(payload), { action: "events", activityId, limit });
56
+ }
57
+ case "start": {
58
+ const title = trimOrNull(params.title);
59
+ if (!title)
60
+ return textResult("start requires title");
61
+ const kind = trimOrNull(params.kind);
62
+ const heartbeatEveryMs = params.heartbeat_every_ms != null && Number.isFinite(params.heartbeat_every_ms)
63
+ ? Math.max(0, Math.trunc(params.heartbeat_every_ms))
64
+ : undefined;
65
+ const payload = await fetchMuJson("/api/activities/start", {
66
+ method: "POST",
67
+ body: {
68
+ title,
69
+ kind,
70
+ heartbeat_every_ms: heartbeatEveryMs,
71
+ },
72
+ });
73
+ return textResult(toJsonText(payload), { action: "start", title, kind, heartbeatEveryMs });
74
+ }
75
+ case "progress": {
76
+ const activityId = trimOrNull(params.activity_id);
77
+ if (!activityId)
78
+ return textResult("progress requires activity_id");
79
+ const message = trimOrNull(params.message) ?? "progress updated";
80
+ const payload = await fetchMuJson("/api/activities/progress", {
81
+ method: "POST",
82
+ body: {
83
+ activity_id: activityId,
84
+ message,
85
+ },
86
+ });
87
+ return textResult(toJsonText(payload), { action: "progress", activityId, message });
88
+ }
89
+ case "heartbeat": {
90
+ const activityId = trimOrNull(params.activity_id);
91
+ if (!activityId)
92
+ return textResult("heartbeat requires activity_id");
93
+ const reason = trimOrNull(params.reason) ?? "manual";
94
+ const payload = await fetchMuJson("/api/activities/heartbeat", {
95
+ method: "POST",
96
+ body: {
97
+ activity_id: activityId,
98
+ reason,
99
+ },
100
+ });
101
+ return textResult(toJsonText(payload), { action: "heartbeat", activityId, reason });
102
+ }
103
+ case "complete":
104
+ case "fail":
105
+ case "cancel": {
106
+ const activityId = trimOrNull(params.activity_id);
107
+ if (!activityId)
108
+ return textResult(`${params.action} requires activity_id`);
109
+ const message = trimOrNull(params.message);
110
+ const payload = await fetchMuJson(`/api/activities/${params.action}`, {
111
+ method: "POST",
112
+ body: {
113
+ activity_id: activityId,
114
+ message,
115
+ },
116
+ });
117
+ return textResult(toJsonText(payload), { action: params.action, activityId, message });
118
+ }
119
+ default:
120
+ return textResult(`unknown action: ${params.action}`);
121
+ }
122
+ },
123
+ });
124
+ }
125
+ export default activitiesExtension;
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function heartbeatsExtension(pi: ExtensionAPI): void;
3
+ export default heartbeatsExtension;
4
+ //# sourceMappingURL=heartbeats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeats.d.ts","sourceRoot":"","sources":["../../src/extensions/heartbeats.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAUlE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,QAyInD;AAED,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { clampInt, fetchMuJson, textResult, toJsonText } from "./shared.js";
4
+ function trimOrNull(value) {
5
+ if (value == null)
6
+ return null;
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : null;
9
+ }
10
+ export function heartbeatsExtension(pi) {
11
+ const Params = Type.Object({
12
+ action: StringEnum(["list", "get", "create", "update", "delete", "trigger", "enable", "disable"]),
13
+ program_id: Type.Optional(Type.String({ description: "Heartbeat program ID" })),
14
+ title: Type.Optional(Type.String({ description: "Program title" })),
15
+ target_kind: Type.Optional(Type.String({ description: "Target kind (run|activity)" })),
16
+ run_job_id: Type.Optional(Type.String({ description: "Run job ID target" })),
17
+ run_root_issue_id: Type.Optional(Type.String({ description: "Run root issue ID target" })),
18
+ activity_id: Type.Optional(Type.String({ description: "Activity ID target" })),
19
+ every_ms: Type.Optional(Type.Number({ description: "Heartbeat interval in ms" })),
20
+ reason: Type.Optional(Type.String({ description: "Heartbeat reason" })),
21
+ enabled: Type.Optional(Type.Boolean({ description: "Enabled state" })),
22
+ limit: Type.Optional(Type.Number({ description: "Max returned items for list" })),
23
+ });
24
+ pi.registerTool({
25
+ name: "mu_heartbeats",
26
+ label: "Heartbeats",
27
+ description: "Program and manage persistent heartbeat schedules. Actions: list, get, create, update, delete, trigger, enable, disable.",
28
+ parameters: Params,
29
+ async execute(_toolCallId, params) {
30
+ switch (params.action) {
31
+ case "list": {
32
+ const query = new URLSearchParams();
33
+ const targetKind = trimOrNull(params.target_kind);
34
+ if (targetKind) {
35
+ query.set("target_kind", targetKind);
36
+ }
37
+ if (typeof params.enabled === "boolean") {
38
+ query.set("enabled", params.enabled ? "true" : "false");
39
+ }
40
+ query.set("limit", String(clampInt(params.limit, 50, 1, 500)));
41
+ const payload = await fetchMuJson(`/api/heartbeats?${query.toString()}`);
42
+ return textResult(toJsonText(payload), {
43
+ action: "list",
44
+ targetKind,
45
+ enabled: params.enabled,
46
+ });
47
+ }
48
+ case "get": {
49
+ const programId = trimOrNull(params.program_id);
50
+ if (!programId)
51
+ return textResult("get requires program_id");
52
+ const payload = await fetchMuJson(`/api/heartbeats/${encodeURIComponent(programId)}`);
53
+ return textResult(toJsonText(payload), { action: "get", programId });
54
+ }
55
+ case "create": {
56
+ const title = trimOrNull(params.title);
57
+ const targetKind = trimOrNull(params.target_kind);
58
+ if (!title)
59
+ return textResult("create requires title");
60
+ if (!targetKind)
61
+ return textResult("create requires target_kind (run|activity)");
62
+ const payload = await fetchMuJson("/api/heartbeats/create", {
63
+ method: "POST",
64
+ body: {
65
+ title,
66
+ target_kind: targetKind,
67
+ run_job_id: trimOrNull(params.run_job_id),
68
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
69
+ activity_id: trimOrNull(params.activity_id),
70
+ every_ms: params.every_ms != null && Number.isFinite(params.every_ms)
71
+ ? Math.max(0, Math.trunc(params.every_ms))
72
+ : undefined,
73
+ reason: trimOrNull(params.reason),
74
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
75
+ },
76
+ });
77
+ return textResult(toJsonText(payload), { action: "create", title, targetKind });
78
+ }
79
+ case "update": {
80
+ const programId = trimOrNull(params.program_id);
81
+ if (!programId)
82
+ return textResult("update requires program_id");
83
+ const payload = await fetchMuJson("/api/heartbeats/update", {
84
+ method: "POST",
85
+ body: {
86
+ program_id: programId,
87
+ title: trimOrNull(params.title),
88
+ target_kind: trimOrNull(params.target_kind),
89
+ run_job_id: trimOrNull(params.run_job_id),
90
+ run_root_issue_id: trimOrNull(params.run_root_issue_id),
91
+ activity_id: trimOrNull(params.activity_id),
92
+ every_ms: params.every_ms != null && Number.isFinite(params.every_ms)
93
+ ? Math.max(0, Math.trunc(params.every_ms))
94
+ : undefined,
95
+ reason: trimOrNull(params.reason),
96
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
97
+ },
98
+ });
99
+ return textResult(toJsonText(payload), { action: "update", programId });
100
+ }
101
+ case "delete": {
102
+ const programId = trimOrNull(params.program_id);
103
+ if (!programId)
104
+ return textResult("delete requires program_id");
105
+ const payload = await fetchMuJson("/api/heartbeats/delete", {
106
+ method: "POST",
107
+ body: {
108
+ program_id: programId,
109
+ },
110
+ });
111
+ return textResult(toJsonText(payload), { action: "delete", programId });
112
+ }
113
+ case "trigger": {
114
+ const programId = trimOrNull(params.program_id);
115
+ if (!programId)
116
+ return textResult("trigger requires program_id");
117
+ const payload = await fetchMuJson("/api/heartbeats/trigger", {
118
+ method: "POST",
119
+ body: {
120
+ program_id: programId,
121
+ reason: trimOrNull(params.reason),
122
+ },
123
+ });
124
+ return textResult(toJsonText(payload), { action: "trigger", programId });
125
+ }
126
+ case "enable":
127
+ case "disable": {
128
+ const programId = trimOrNull(params.program_id);
129
+ if (!programId)
130
+ return textResult(`${params.action} requires program_id`);
131
+ const payload = await fetchMuJson("/api/heartbeats/update", {
132
+ method: "POST",
133
+ body: {
134
+ program_id: programId,
135
+ enabled: params.action === "enable",
136
+ },
137
+ });
138
+ return textResult(toJsonText(payload), {
139
+ action: params.action,
140
+ programId,
141
+ });
142
+ }
143
+ default:
144
+ return textResult(`unknown action: ${params.action}`);
145
+ }
146
+ },
147
+ });
148
+ }
149
+ export default heartbeatsExtension;
@@ -1,6 +1,9 @@
1
+ export { activitiesExtension } from "./activities.js";
1
2
  export { brandingExtension } from "./branding.js";
2
3
  export { eventLogExtension } from "./event-log.js";
4
+ export { heartbeatsExtension } from "./heartbeats.js";
3
5
  export { messagingSetupExtension } from "./messaging-setup.js";
6
+ export { orchestrationRunsExtension } from "./orchestration-runs.js";
4
7
  export { serverToolsExtension } from "./server-tools.js";
5
8
  /**
6
9
  * Serve-mode extension module paths.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AASzD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAiBzD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAE/B,CAAC"}
@@ -1,8 +1,19 @@
1
+ export { activitiesExtension } from "./activities.js";
1
2
  export { brandingExtension } from "./branding.js";
2
3
  export { eventLogExtension } from "./event-log.js";
4
+ export { heartbeatsExtension } from "./heartbeats.js";
3
5
  export { messagingSetupExtension } from "./messaging-setup.js";
6
+ export { orchestrationRunsExtension } from "./orchestration-runs.js";
4
7
  export { serverToolsExtension } from "./server-tools.js";
5
- const SERVE_EXTENSION_MODULE_BASENAMES = ["branding", "server-tools", "event-log", "messaging-setup"];
8
+ const SERVE_EXTENSION_MODULE_BASENAMES = [
9
+ "branding",
10
+ "server-tools",
11
+ "event-log",
12
+ "messaging-setup",
13
+ "orchestration-runs",
14
+ "activities",
15
+ "heartbeats",
16
+ ];
6
17
  const RUNTIME_EXTENSION = import.meta.url.endsWith(".ts") ? "ts" : "js";
7
18
  function resolveBundledExtensionPath(moduleBasename) {
8
19
  return new URL(`./${moduleBasename}.${RUNTIME_EXTENSION}`, import.meta.url).pathname;
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export declare function orchestrationRunsExtension(pi: ExtensionAPI): void;
3
+ export default orchestrationRunsExtension;
4
+ //# sourceMappingURL=orchestration-runs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestration-runs.d.ts","sourceRoot":"","sources":["../../src/extensions/orchestration-runs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAUlE,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,YAAY,QAiH1D;AAED,eAAe,0BAA0B,CAAC"}
@@ -0,0 +1,123 @@
1
+ import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { clampInt, fetchMuJson, textResult, toJsonText } from "./shared.js";
4
+ function trimOrNull(value) {
5
+ if (value == null)
6
+ return null;
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : null;
9
+ }
10
+ export function orchestrationRunsExtension(pi) {
11
+ const RunsParams = Type.Object({
12
+ action: StringEnum(["list", "status", "start", "resume", "interrupt", "heartbeat", "trace"]),
13
+ job_id: Type.Optional(Type.String({ description: "Run job ID" })),
14
+ root_issue_id: Type.Optional(Type.String({ description: "Run root issue ID (mu-...)" })),
15
+ prompt: Type.Optional(Type.String({ description: "Prompt for run start" })),
16
+ max_steps: Type.Optional(Type.Number({ description: "Optional max steps for start/resume" })),
17
+ limit: Type.Optional(Type.Number({ description: "Optional limit (list/trace)" })),
18
+ status: Type.Optional(Type.String({ description: "Optional status filter for list" })),
19
+ reason: Type.Optional(Type.String({ description: "Optional heartbeat reason (default: manual)" })),
20
+ });
21
+ pi.registerTool({
22
+ name: "mu_runs",
23
+ label: "Runs",
24
+ description: "Manage orchestration runs. Actions: list, status, start, resume, interrupt, heartbeat, trace (stdout/stderr + .mu/log hints).",
25
+ parameters: RunsParams,
26
+ async execute(_toolCallId, params) {
27
+ switch (params.action) {
28
+ case "list": {
29
+ const query = new URLSearchParams();
30
+ const status = trimOrNull(params.status);
31
+ if (status)
32
+ query.set("status", status);
33
+ const limit = clampInt(params.limit, 50, 1, 500);
34
+ query.set("limit", String(limit));
35
+ const payload = await fetchMuJson(`/api/runs?${query.toString()}`);
36
+ return textResult(toJsonText(payload), { action: "list", status, limit });
37
+ }
38
+ case "status": {
39
+ const id = trimOrNull(params.job_id) ?? trimOrNull(params.root_issue_id);
40
+ if (!id)
41
+ return textResult("status requires job_id or root_issue_id");
42
+ const payload = await fetchMuJson(`/api/runs/${encodeURIComponent(id)}`);
43
+ return textResult(toJsonText(payload), { action: "status", id });
44
+ }
45
+ case "start": {
46
+ const prompt = trimOrNull(params.prompt);
47
+ if (!prompt)
48
+ return textResult("start requires prompt");
49
+ const maxSteps = params.max_steps != null && Number.isFinite(params.max_steps)
50
+ ? Math.max(1, Math.trunc(params.max_steps))
51
+ : undefined;
52
+ const payload = await fetchMuJson("/api/runs/start", {
53
+ method: "POST",
54
+ body: {
55
+ prompt,
56
+ max_steps: maxSteps,
57
+ },
58
+ });
59
+ return textResult(toJsonText(payload), { action: "start", prompt, maxSteps });
60
+ }
61
+ case "resume": {
62
+ const rootIssueId = trimOrNull(params.root_issue_id);
63
+ if (!rootIssueId)
64
+ return textResult("resume requires root_issue_id");
65
+ const maxSteps = params.max_steps != null && Number.isFinite(params.max_steps)
66
+ ? Math.max(1, Math.trunc(params.max_steps))
67
+ : undefined;
68
+ const payload = await fetchMuJson("/api/runs/resume", {
69
+ method: "POST",
70
+ body: {
71
+ root_issue_id: rootIssueId,
72
+ max_steps: maxSteps,
73
+ },
74
+ });
75
+ return textResult(toJsonText(payload), { action: "resume", rootIssueId, maxSteps });
76
+ }
77
+ case "interrupt": {
78
+ const jobId = trimOrNull(params.job_id);
79
+ const rootIssueId = trimOrNull(params.root_issue_id);
80
+ if (!jobId && !rootIssueId) {
81
+ return textResult("interrupt requires job_id or root_issue_id");
82
+ }
83
+ const payload = await fetchMuJson("/api/runs/interrupt", {
84
+ method: "POST",
85
+ body: {
86
+ job_id: jobId,
87
+ root_issue_id: rootIssueId,
88
+ },
89
+ });
90
+ return textResult(toJsonText(payload), { action: "interrupt", jobId, rootIssueId });
91
+ }
92
+ case "heartbeat": {
93
+ const jobId = trimOrNull(params.job_id);
94
+ const rootIssueId = trimOrNull(params.root_issue_id);
95
+ if (!jobId && !rootIssueId) {
96
+ return textResult("heartbeat requires job_id or root_issue_id");
97
+ }
98
+ const reason = trimOrNull(params.reason) ?? "manual";
99
+ const payload = await fetchMuJson("/api/runs/heartbeat", {
100
+ method: "POST",
101
+ body: {
102
+ job_id: jobId,
103
+ root_issue_id: rootIssueId,
104
+ reason,
105
+ },
106
+ });
107
+ return textResult(toJsonText(payload), { action: "heartbeat", jobId, rootIssueId, reason });
108
+ }
109
+ case "trace": {
110
+ const id = trimOrNull(params.job_id) ?? trimOrNull(params.root_issue_id);
111
+ if (!id)
112
+ return textResult("trace requires job_id or root_issue_id");
113
+ const limit = clampInt(params.limit, 200, 1, 2_000);
114
+ const payload = await fetchMuJson(`/api/runs/${encodeURIComponent(id)}/trace?limit=${limit}`);
115
+ return textResult(toJsonText(payload), { action: "trace", id, limit });
116
+ }
117
+ default:
118
+ return textResult(`unknown action: ${params.action}`);
119
+ }
120
+ },
121
+ });
122
+ }
123
+ export default orchestrationRunsExtension;
@@ -53,7 +53,7 @@ export function serverToolsExtension(pi) {
53
53
  const extra = [
54
54
  "",
55
55
  `[MU SERVER] Connected at ${url}.`,
56
- "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events.",
56
+ "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats.",
57
57
  "Use these tools to inspect repository state and control-plane runtime before advising users.",
58
58
  ].join("\n");
59
59
  return {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { CommandContextResolver } from "./command_context.js";
3
+ import { type CreateMuSessionOpts, type MuSession } from "./session_factory.js";
3
4
  export type MessagingOperatorInboundEnvelope = {
4
5
  channel: string;
5
6
  channel_tenant_id: string;
@@ -30,10 +31,18 @@ export declare const OperatorApprovedCommandSchema: z.ZodDiscriminatedUnion<[z.Z
30
31
  kind: z.ZodLiteral<"forum_read">;
31
32
  topic: z.ZodOptional<z.ZodString>;
32
33
  limit: z.ZodOptional<z.ZodNumber>;
34
+ }, z.core.$strip>, z.ZodObject<{
35
+ kind: z.ZodLiteral<"run_list">;
36
+ }, z.core.$strip>, z.ZodObject<{
37
+ kind: z.ZodLiteral<"run_status">;
38
+ root_issue_id: z.ZodOptional<z.ZodString>;
33
39
  }, z.core.$strip>, z.ZodObject<{
34
40
  kind: z.ZodLiteral<"run_resume">;
35
41
  root_issue_id: z.ZodOptional<z.ZodString>;
36
42
  max_steps: z.ZodOptional<z.ZodNumber>;
43
+ }, z.core.$strip>, z.ZodObject<{
44
+ kind: z.ZodLiteral<"run_interrupt">;
45
+ root_issue_id: z.ZodOptional<z.ZodString>;
37
46
  }, z.core.$strip>, z.ZodObject<{
38
47
  kind: z.ZodLiteral<"run_start">;
39
48
  prompt: z.ZodString;
@@ -58,10 +67,18 @@ export declare const OperatorBackendTurnResultSchema: z.ZodDiscriminatedUnion<[z
58
67
  kind: z.ZodLiteral<"forum_read">;
59
68
  topic: z.ZodOptional<z.ZodString>;
60
69
  limit: z.ZodOptional<z.ZodNumber>;
70
+ }, z.core.$strip>, z.ZodObject<{
71
+ kind: z.ZodLiteral<"run_list">;
72
+ }, z.core.$strip>, z.ZodObject<{
73
+ kind: z.ZodLiteral<"run_status">;
74
+ root_issue_id: z.ZodOptional<z.ZodString>;
61
75
  }, z.core.$strip>, z.ZodObject<{
62
76
  kind: z.ZodLiteral<"run_resume">;
63
77
  root_issue_id: z.ZodOptional<z.ZodString>;
64
78
  max_steps: z.ZodOptional<z.ZodNumber>;
79
+ }, z.core.$strip>, z.ZodObject<{
80
+ kind: z.ZodLiteral<"run_interrupt">;
81
+ root_issue_id: z.ZodOptional<z.ZodString>;
65
82
  }, z.core.$strip>, z.ZodObject<{
66
83
  kind: z.ZodLiteral<"run_start">;
67
84
  prompt: z.ZodString;
@@ -77,6 +94,7 @@ export type OperatorBackendTurnInput = {
77
94
  };
78
95
  export interface MessagingOperatorBackend {
79
96
  runTurn(input: OperatorBackendTurnInput): Promise<OperatorBackendTurnResult>;
97
+ dispose?(): void | Promise<void>;
80
98
  }
81
99
  export type OperatorDecision = {
82
100
  kind: "response";
@@ -129,6 +147,7 @@ export declare class MessagingOperatorRuntime {
129
147
  inbound: InboundEnvelope;
130
148
  binding: IdentityBinding;
131
149
  }): Promise<OperatorDecision>;
150
+ stop(): Promise<void>;
132
151
  }
133
152
  export type PiMessagingOperatorBackendOpts = {
134
153
  provider?: string;
@@ -137,12 +156,17 @@ export type PiMessagingOperatorBackendOpts = {
137
156
  systemPrompt?: string;
138
157
  timeoutMs?: number;
139
158
  extensionPaths?: string[];
159
+ sessionFactory?: (opts: CreateMuSessionOpts) => Promise<MuSession>;
160
+ nowMs?: () => number;
161
+ sessionIdleTtlMs?: number;
162
+ maxSessions?: number;
140
163
  };
141
164
  export declare const DEFAULT_CHAT_SYSTEM_PROMPT: string;
142
165
  export declare class PiMessagingOperatorBackend implements MessagingOperatorBackend {
143
166
  #private;
144
167
  constructor(opts?: PiMessagingOperatorBackendOpts);
145
168
  runTurn(input: OperatorBackendTurnInput): Promise<OperatorBackendTurnResult>;
169
+ dispose(): void;
146
170
  }
147
171
  export {};
148
172
  //# sourceMappingURL=operator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../src/operator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../src/operator.ts"],"names":[],"mappings":"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"}
package/dist/operator.js CHANGED
@@ -12,11 +12,20 @@ export const OperatorApprovedCommandSchema = z.discriminatedUnion("kind", [
12
12
  topic: z.string().trim().min(1).optional(),
13
13
  limit: z.number().int().min(1).max(500).optional(),
14
14
  }),
15
+ z.object({ kind: z.literal("run_list") }),
16
+ z.object({
17
+ kind: z.literal("run_status"),
18
+ root_issue_id: z.string().trim().min(1).optional(),
19
+ }),
15
20
  z.object({
16
21
  kind: z.literal("run_resume"),
17
22
  root_issue_id: z.string().trim().min(1).optional(),
18
23
  max_steps: z.number().int().min(1).max(500).optional(),
19
24
  }),
25
+ z.object({
26
+ kind: z.literal("run_interrupt"),
27
+ root_issue_id: z.string().trim().min(1).optional(),
28
+ }),
20
29
  z.object({
21
30
  kind: z.literal("run_start"),
22
31
  prompt: z.string().trim().min(1),
@@ -74,6 +83,17 @@ export class ApprovedCommandBroker {
74
83
  }
75
84
  break;
76
85
  }
86
+ case "run_list":
87
+ commandKey = "run list";
88
+ args = [];
89
+ break;
90
+ case "run_status":
91
+ commandKey = "run status";
92
+ args = [];
93
+ if (opts.proposal.root_issue_id) {
94
+ args.push(normalizeArg(opts.proposal.root_issue_id));
95
+ }
96
+ break;
77
97
  case "run_resume": {
78
98
  if (!this.#runTriggersEnabled) {
79
99
  return { kind: "reject", reason: "operator_action_disallowed", details: "run triggers disabled" };
@@ -88,6 +108,17 @@ export class ApprovedCommandBroker {
88
108
  }
89
109
  break;
90
110
  }
111
+ case "run_interrupt": {
112
+ if (!this.#runTriggersEnabled) {
113
+ return { kind: "reject", reason: "operator_action_disallowed", details: "run triggers disabled" };
114
+ }
115
+ commandKey = "run interrupt";
116
+ args = [];
117
+ if (opts.proposal.root_issue_id) {
118
+ args.push(normalizeArg(opts.proposal.root_issue_id));
119
+ }
120
+ break;
121
+ }
91
122
  case "run_start": {
92
123
  if (!this.#runTriggersEnabled) {
93
124
  return { kind: "reject", reason: "operator_action_disallowed", details: "run triggers disabled" };
@@ -230,6 +261,10 @@ export class MessagingOperatorRuntime {
230
261
  operatorTurnId: turnId,
231
262
  };
232
263
  }
264
+ async stop() {
265
+ this.#sessionByConversation.clear();
266
+ await this.#backend.dispose?.();
267
+ }
233
268
  }
234
269
  export const DEFAULT_CHAT_SYSTEM_PROMPT = [
235
270
  "You are mu, an AI assistant for the mu orchestration platform.",
@@ -242,16 +277,53 @@ export const DEFAULT_CHAT_SYSTEM_PROMPT = [
242
277
  "",
243
278
  "Be concise, practical, and actionable.",
244
279
  ].join("\n");
280
+ const OPERATOR_COMMAND_PREFIX = "MU_COMMAND:";
245
281
  const DEFAULT_OPERATOR_SYSTEM_PROMPT = [
246
282
  "You are mu, an AI assistant for the mu orchestration platform.",
247
283
  "You have tools to interact with the mu server: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events.",
248
284
  "Use these tools to answer questions about repository state, issues, events, and control-plane runtime state.",
249
285
  "For adapter setup workflow, use mu_messaging_setup (check/preflight/plan/apply/verify/guide).",
250
286
  "You can help users set up messaging integrations (Slack, Discord, Telegram, Gmail planning).",
287
+ "You may either respond normally or emit an approved control-plane command.",
288
+ `To emit a command, output exactly one line with prefix ${OPERATOR_COMMAND_PREFIX} followed by compact JSON.`,
289
+ "Example:",
290
+ `MU_COMMAND: {\"kind\":\"run_start\",\"prompt\":\"ship release\"}`,
291
+ "Available command kinds: status, ready, issue_list, issue_get, forum_read, run_list, run_status, run_start, run_resume, run_interrupt.",
251
292
  "",
252
293
  "Be concise, practical, and actionable.",
253
- "Respond in plain text do not wrap in JSON.",
294
+ "For normal conversational answers, respond in plain text.",
254
295
  ].join("\n");
296
+ function parseOperatorCommandDirective(text) {
297
+ const whole = text.trim();
298
+ if (whole.startsWith("{") && whole.endsWith("}")) {
299
+ try {
300
+ return OperatorApprovedCommandSchema.parse(JSON.parse(whole));
301
+ }
302
+ catch {
303
+ // fall through to explicit MU_COMMAND parsing
304
+ }
305
+ }
306
+ const lines = text.split(/\r?\n/);
307
+ for (const line of lines) {
308
+ const trimmed = line.trim();
309
+ if (!trimmed.startsWith(OPERATOR_COMMAND_PREFIX)) {
310
+ continue;
311
+ }
312
+ const payloadText = trimmed.slice(OPERATOR_COMMAND_PREFIX.length).trim();
313
+ if (payloadText.length === 0) {
314
+ throw new Error("operator_command_directive_missing_payload");
315
+ }
316
+ let parsed;
317
+ try {
318
+ parsed = JSON.parse(payloadText);
319
+ }
320
+ catch (err) {
321
+ throw new Error(`operator_command_directive_invalid_json: ${err instanceof Error ? err.message : String(err)}`);
322
+ }
323
+ return OperatorApprovedCommandSchema.parse(parsed);
324
+ }
325
+ return null;
326
+ }
255
327
  function buildOperatorPrompt(input) {
256
328
  return [
257
329
  `[Messaging context]`,
@@ -269,6 +341,11 @@ export class PiMessagingOperatorBackend {
269
341
  #systemPrompt;
270
342
  #timeoutMs;
271
343
  #extensionPaths;
344
+ #sessionFactory;
345
+ #nowMs;
346
+ #sessionIdleTtlMs;
347
+ #maxSessions;
348
+ #sessions = new Map();
272
349
  constructor(opts = {}) {
273
350
  this.#provider = opts.provider;
274
351
  this.#model = opts.model;
@@ -276,71 +353,136 @@ export class PiMessagingOperatorBackend {
276
353
  this.#systemPrompt = opts.systemPrompt ?? DEFAULT_OPERATOR_SYSTEM_PROMPT;
277
354
  this.#timeoutMs = Math.max(1_000, Math.trunc(opts.timeoutMs ?? 90_000));
278
355
  this.#extensionPaths = opts.extensionPaths ?? [];
356
+ this.#sessionFactory = opts.sessionFactory ?? createMuSession;
357
+ this.#nowMs = opts.nowMs ?? Date.now;
358
+ this.#sessionIdleTtlMs = Math.max(60_000, Math.trunc(opts.sessionIdleTtlMs ?? 30 * 60 * 1_000));
359
+ this.#maxSessions = Math.max(1, Math.trunc(opts.maxSessions ?? 32));
279
360
  }
280
- async runTurn(input) {
281
- const session = await createMuSession({
282
- cwd: input.inbound.repo_root,
361
+ #disposeSession(sessionId) {
362
+ const entry = this.#sessions.get(sessionId);
363
+ if (!entry) {
364
+ return;
365
+ }
366
+ this.#sessions.delete(sessionId);
367
+ try {
368
+ entry.session.dispose();
369
+ }
370
+ catch {
371
+ // Best effort cleanup.
372
+ }
373
+ }
374
+ #pruneSessions(nowMs) {
375
+ for (const [sessionId, entry] of this.#sessions.entries()) {
376
+ if (nowMs - entry.lastUsedAtMs > this.#sessionIdleTtlMs) {
377
+ this.#disposeSession(sessionId);
378
+ }
379
+ }
380
+ if (this.#sessions.size <= this.#maxSessions) {
381
+ return;
382
+ }
383
+ const byOldestUse = [...this.#sessions.entries()].sort((a, b) => a[1].lastUsedAtMs - b[1].lastUsedAtMs);
384
+ while (this.#sessions.size > this.#maxSessions && byOldestUse.length > 0) {
385
+ const [sessionId] = byOldestUse.shift();
386
+ this.#disposeSession(sessionId);
387
+ }
388
+ }
389
+ async #createSession(repoRoot, nowMs) {
390
+ const session = await this.#sessionFactory({
391
+ cwd: repoRoot,
283
392
  systemPrompt: this.#systemPrompt,
284
393
  provider: this.#provider,
285
394
  model: this.#model,
286
395
  thinking: this.#thinking,
287
396
  extensionPaths: this.#extensionPaths,
288
397
  });
289
- try {
290
- await session.bindExtensions({
291
- commandContextActions: {
292
- waitForIdle: () => session.agent.waitForIdle(),
293
- newSession: async () => ({ cancelled: true }),
294
- fork: async () => ({ cancelled: true }),
295
- navigateTree: async () => ({ cancelled: true }),
296
- switchSession: async () => ({ cancelled: true }),
297
- reload: async () => { },
298
- },
299
- onError: () => { },
300
- });
301
- let assistantText = "";
302
- const unsub = session.subscribe((event) => {
303
- if (event?.type === "message_end" && event?.message?.role === "assistant") {
304
- const msg = event.message;
305
- if (typeof msg.text === "string") {
306
- assistantText = msg.text;
307
- }
308
- else if (typeof msg.content === "string") {
309
- assistantText = msg.content;
310
- }
311
- else if (Array.isArray(msg.content)) {
312
- const parts = [];
313
- for (const item of msg.content) {
314
- const t = typeof item === "string" ? item : item?.text;
315
- if (typeof t === "string" && t.trim().length > 0)
316
- parts.push(t);
317
- }
318
- if (parts.length > 0)
319
- assistantText = parts.join("\n");
398
+ await session.bindExtensions({
399
+ commandContextActions: {
400
+ waitForIdle: () => session.agent.waitForIdle(),
401
+ newSession: async () => ({ cancelled: true }),
402
+ fork: async () => ({ cancelled: true }),
403
+ navigateTree: async () => ({ cancelled: true }),
404
+ switchSession: async () => ({ cancelled: true }),
405
+ reload: async () => { },
406
+ },
407
+ onError: () => { },
408
+ });
409
+ return {
410
+ session,
411
+ repoRoot,
412
+ createdAtMs: nowMs,
413
+ lastUsedAtMs: nowMs,
414
+ };
415
+ }
416
+ async #resolveSession(sessionId, repoRoot) {
417
+ const nowMs = Math.trunc(this.#nowMs());
418
+ this.#pruneSessions(nowMs);
419
+ const existing = this.#sessions.get(sessionId);
420
+ if (existing && existing.repoRoot === repoRoot) {
421
+ existing.lastUsedAtMs = nowMs;
422
+ return existing;
423
+ }
424
+ if (existing && existing.repoRoot !== repoRoot) {
425
+ this.#disposeSession(sessionId);
426
+ }
427
+ const created = await this.#createSession(repoRoot, nowMs);
428
+ this.#sessions.set(sessionId, created);
429
+ this.#pruneSessions(nowMs);
430
+ return created;
431
+ }
432
+ async runTurn(input) {
433
+ const sessionRecord = await this.#resolveSession(input.sessionId, input.inbound.repo_root);
434
+ const session = sessionRecord.session;
435
+ let assistantText = "";
436
+ const unsub = session.subscribe((event) => {
437
+ if (event?.type === "message_end" && event?.message?.role === "assistant") {
438
+ const msg = event.message;
439
+ if (typeof msg.text === "string") {
440
+ assistantText = msg.text;
441
+ }
442
+ else if (typeof msg.content === "string") {
443
+ assistantText = msg.content;
444
+ }
445
+ else if (Array.isArray(msg.content)) {
446
+ const parts = [];
447
+ for (const item of msg.content) {
448
+ const t = typeof item === "string" ? item : item?.text;
449
+ if (typeof t === "string" && t.trim().length > 0)
450
+ parts.push(t);
320
451
  }
452
+ if (parts.length > 0)
453
+ assistantText = parts.join("\n");
321
454
  }
322
- });
323
- const timeoutPromise = new Promise((_, reject) => {
324
- setTimeout(() => reject(new Error("pi operator timeout")), this.#timeoutMs);
325
- });
326
- try {
327
- await Promise.race([
328
- session.prompt(buildOperatorPrompt(input), { expandPromptTemplates: false }),
329
- timeoutPromise,
330
- ]);
331
- }
332
- finally {
333
- unsub();
334
- }
335
- // The agent now responds naturally with tools — always return as respond
336
- const message = assistantText.trim();
337
- if (!message) {
338
- throw new Error("operator_empty_response");
339
455
  }
340
- return { kind: "respond", message: message.slice(0, 2000) };
456
+ });
457
+ const timeoutPromise = new Promise((_, reject) => {
458
+ setTimeout(() => reject(new Error("pi operator timeout")), this.#timeoutMs);
459
+ });
460
+ try {
461
+ await Promise.race([
462
+ session.prompt(buildOperatorPrompt(input), { expandPromptTemplates: false }),
463
+ timeoutPromise,
464
+ ]);
341
465
  }
342
466
  finally {
343
- session.dispose();
467
+ unsub();
468
+ sessionRecord.lastUsedAtMs = Math.trunc(this.#nowMs());
469
+ }
470
+ const message = assistantText.trim();
471
+ if (!message) {
472
+ throw new Error("operator_empty_response");
473
+ }
474
+ const command = parseOperatorCommandDirective(message);
475
+ if (command) {
476
+ return {
477
+ kind: "command",
478
+ command,
479
+ };
480
+ }
481
+ return { kind: "respond", message: message.slice(0, 2000) };
482
+ }
483
+ dispose() {
484
+ for (const sessionId of [...this.#sessions.keys()]) {
485
+ this.#disposeSession(sessionId);
344
486
  }
345
487
  }
346
488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-agent",
3
- "version": "26.2.41",
3
+ "version": "26.2.42",
4
4
  "description": "Shared agent runtime for mu chat, orchestration roles, and serve extensions.",
5
5
  "keywords": [
6
6
  "mu",
@@ -22,7 +22,7 @@
22
22
  "dist/**"
23
23
  ],
24
24
  "dependencies": {
25
- "@femtomc/mu-core": "26.2.41",
25
+ "@femtomc/mu-core": "26.2.42",
26
26
  "@mariozechner/pi-agent-core": "^0.52.12",
27
27
  "@mariozechner/pi-ai": "^0.52.12",
28
28
  "@mariozechner/pi-coding-agent": "^0.52.12",