@agimon-ai/doompi-loop 0.0.1-alpha.83 → 0.0.1-alpha.85

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,214 +1,204 @@
1
- import { a as LIST_COMMAND_NAME, n as formatLoopStatusView, r as createDoomLoopLaunchersService, s as START_COMMAND_NAME, t as LOOP_VIEW_STATUS_KEY } from "../loopView-CmuIEbUa.mjs";
2
- import { composeRootHooks, defineRoot, defineRoutedContribution } from "@agimon-ai/doompi-core/extensionFile";
1
+ import { t as DOOM_LOOP_LAUNCHERS_SERVICE } from "../loopLaunchers-cCniFbVA.mjs";
2
+ import { _ as PACKAGE_SOURCE, a as CronLoopSchema, c as parseIntervalLoop, f as START_COMMAND_NAME, i as createDoomLoopLaunchersService, m as MODE_STATUS_ID, n as formatLoopStatusView, o as IntervalLoopSchema, r as createLoopTools, s as parseCronLoop, t as LOOP_VIEW_STATUS_KEY, u as LIST_COMMAND_NAME } from "../loopView-BK4X_1UI.mjs";
3
+ import { composeRootHooks, defineRoot, defineRoutedContribution, defineTool } from "@agimon-ai/doompi-core/extensionFile";
3
4
  import { defineMinorMode, serverMinorModes } from "@agimon-ai/doompi-minor-mode";
5
+ import { Cron } from "croner";
4
6
  import { defineServerPlugin, readPackageResource } from "@agimon-ai/doompi-core/serverFacet";
5
7
  //#region src/services/serverRuntime/index.ts
6
- const SOURCE = "@agimon-ai/doompi-loop";
7
- const MODE_ID = "loop.active";
8
- const DEFAULT_LAUNCHER_ID = "doompi.default";
9
- const DEFAULT_INTERVAL_SECONDS = 300;
10
- const MIN_INTERVAL_SECONDS = 30;
11
- const MAX_INTERVAL_SECONDS = 3600;
12
- function notify(context, body, level = "info") {
13
- return Promise.resolve(context.client.notify({
14
- body,
15
- level
16
- }));
17
- }
18
- function numberInput(value, fallback) {
19
- if (typeof value !== "string" || value.trim() === "") return fallback;
20
- const parsed = Number(value);
21
- if (!Number.isInteger(parsed) || parsed < MIN_INTERVAL_SECONDS || parsed > MAX_INTERVAL_SECONDS) throw new Error(`Interval must be between ${MIN_INTERVAL_SECONDS} and ${MAX_INTERVAL_SECONDS} seconds.`);
22
- return parsed;
8
+ async function prepareLoop(execution, request, cron) {
9
+ if (request.input !== void 0) return cron ? parseCronLoop(request.input) : parseIntervalLoop(request.input);
10
+ if (request.interactive === false) throw new Error("Loop configuration is required for agent launches.");
11
+ const input = await execution.client.request({
12
+ kind: "input",
13
+ title: "Loop prompt",
14
+ multiline: true
15
+ }, request.signal);
16
+ const prompt = typeof input === "string" ? input.trim() : "";
17
+ if (!prompt || request.signal.aborted) return void 0;
18
+ if (cron) {
19
+ const expression = await execution.client.request({
20
+ kind: "input",
21
+ title: "Cron expression (five fields)",
22
+ initialValue: "0 * * * *"
23
+ }, request.signal);
24
+ if (typeof expression !== "string" || request.signal.aborted) return void 0;
25
+ const timezone = await execution.client.request({
26
+ kind: "input",
27
+ title: "Loop timezone",
28
+ initialValue: "UTC"
29
+ }, request.signal);
30
+ if (typeof timezone !== "string" || request.signal.aborted) return void 0;
31
+ return parseCronLoop({
32
+ prompt,
33
+ cron: expression,
34
+ timezone: timezone.trim() || "UTC"
35
+ });
36
+ }
37
+ const interval = await execution.client.request({
38
+ kind: "input",
39
+ title: "Loop interval in seconds",
40
+ initialValue: "300"
41
+ }, request.signal);
42
+ if (typeof interval !== "string" || request.signal.aborted) return void 0;
43
+ return parseIntervalLoop({
44
+ prompt,
45
+ intervalSeconds: Number(interval.trim() || "300")
46
+ });
23
47
  }
24
48
  function createSessionState(host) {
25
- let launchers;
26
- let defaultRegistration;
27
- let unsubscribe;
28
- let modeOwner;
29
- const modeSelected = () => (host.context.selection.state?.["minor-mode"] ?? []).includes(MODE_ID);
30
- const modeState = () => {
31
- const launcherCount = launchers?.listLaunchers().length ?? 0;
32
- const active = (launchers?.listInstances().length ?? 0) > 0;
33
- const canStart = launcherCount > 0 || !modeSelected();
34
- return {
35
- activation: active ? "active" : "inactive",
36
- condition: launchers?.listInstances().some(({ state }) => state !== "running") ? "queued" : "ready",
37
- ...active ? {
38
- detail: `${launchers?.listInstances().length} active`,
39
- color: "warning"
40
- } : {},
41
- actions: [{
42
- id: "start",
43
- enabled: canStart,
44
- ...canStart ? {} : { disabledReason: "No loop launchers are registered." }
45
- }, {
46
- id: "stop",
47
- enabled: active,
48
- ...active ? {} : { disabledReason: "No loops are active." }
49
- }]
50
- };
51
- };
52
- /**
53
- * The instances the activity dock lists.
54
- *
55
- * An empty list publishes an empty value rather than nothing, because the key
56
- * being present is what keeps the group on screen as a launcher. The group
57
- * declares no `hideWhenEmpty`, so an empty value is a header with no rows.
58
- */
59
- const publishView = () => host.context.client.setStatus(LOOP_VIEW_STATUS_KEY, formatLoopStatusView(launchers?.listInstances() ?? []));
60
- /** Both surfaces a loop shows up on: the minor-mode catalog and the activity dock. */
61
- const publishMode = () => {
62
- modeOwner?.publish();
63
- publishView();
64
- };
65
- const startActivity = async (execution) => {
66
- if (launchers !== void 0) return async () => void 0;
67
- const dependencies = {
68
- generation: `${execution.sessionId}:loop-launchers`,
69
- createInstanceId: () => crypto.randomUUID(),
70
- timestamp: () => (/* @__PURE__ */ new Date()).toISOString()
71
- };
72
- const service = createDoomLoopLaunchersService(dependencies);
73
- const registration = service.register({
74
- id: DEFAULT_LAUNCHER_ID,
75
- source: SOURCE,
76
- label: "Default loop",
77
- description: "Run a prompt in this session on an interval.",
78
- async launch({ instanceId, signal }) {
79
- const promptValue = await execution.client.request({
80
- kind: "input",
81
- title: "Loop prompt",
82
- multiline: true
83
- }, signal);
84
- const prompt = typeof promptValue === "string" ? promptValue.trim() : "";
85
- if (!prompt || signal.aborted) return void 0;
86
- const interval = numberInput(await execution.client.request({
87
- kind: "input",
88
- title: "Loop interval in seconds",
89
- initialValue: String(DEFAULT_INTERVAL_SECONDS)
90
- }, signal), DEFAULT_INTERVAL_SECONDS);
91
- let stopped = false;
92
- const tick = () => {
93
- if (stopped || signal.aborted) return;
94
- execution.session.prompt(prompt).catch((error) => {
95
- notify(execution, `Loop pass failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
96
- });
97
- };
98
- const timer = setInterval(tick, interval * 1e3);
99
- timer.unref?.();
100
- tick();
101
- return {
102
- instanceId,
103
- label: "Default loop",
104
- detail: `every ${interval}s`,
105
- stop() {
106
- stopped = true;
107
- clearInterval(timer);
108
- }
109
- };
110
- }
111
- });
112
- launchers = service;
113
- defaultRegistration = registration;
114
- unsubscribe = service.subscribe(publishMode);
115
- publishMode();
116
- return async () => {
117
- unsubscribe?.();
118
- unsubscribe = void 0;
119
- await registration.dispose("Headless loop activity stopped.");
120
- await service.dispose("Headless loop activity stopped.");
121
- if (launchers === service) launchers = void 0;
122
- if (defaultRegistration === registration) defaultRegistration = void 0;
123
- publishMode();
124
- };
125
- };
126
- modeOwner = defineMinorMode({
49
+ const launchers = createDoomLoopLaunchersService({
50
+ generation: `${host.context.sessionId}:loop-launchers`,
51
+ createInstanceId: () => crypto.randomUUID(),
52
+ timestamp: () => (/* @__PURE__ */ new Date()).toISOString()
53
+ });
54
+ let disposed = false;
55
+ const modeSelected = () => !disposed && (host.context.selection.state?.["minor-mode"] ?? []).includes("loop.active");
56
+ const publishView = () => host.context.client.setStatus(LOOP_VIEW_STATUS_KEY, formatLoopStatusView(launchers.listInstances()));
57
+ const modeOwner = defineMinorMode({
127
58
  descriptor: {
128
- source: SOURCE,
129
- id: MODE_ID,
59
+ source: PACKAGE_SOURCE,
60
+ id: MODE_STATUS_ID,
130
61
  label: "Loop",
131
- description: "Session-scoped recurring prompt loops.",
62
+ description: "Give the agent tools to create, inspect, and stop session loops.",
132
63
  order: 60,
133
64
  actions: [{
134
- id: "start",
135
- label: "Start",
136
- description: "Start a loop with a registered launcher.",
65
+ id: "activate",
66
+ label: "Activate",
67
+ description: "Expose loop tools to the agent.",
137
68
  contexts: ["headless"],
138
- parameters: [{
139
- name: "launcherId",
140
- label: "Launcher",
141
- kind: "string",
142
- required: true,
143
- minLength: 1
144
- }, {
145
- name: "instanceId",
146
- label: "Instance ID",
147
- kind: "string",
148
- required: false,
149
- minLength: 1
150
- }]
69
+ parameters: []
151
70
  }, {
152
- id: "stop",
153
- label: "Stop",
154
- description: "Stop one active loop instance.",
71
+ id: "deactivate",
72
+ label: "Deactivate",
73
+ description: "Hide agent loop tools. Existing loops and manual controls remain available.",
155
74
  contexts: ["headless"],
156
- parameters: [{
157
- name: "instanceId",
158
- label: "Instance ID",
159
- kind: "string",
160
- required: true,
161
- minLength: 1
162
- }, {
163
- name: "reason",
164
- label: "Reason",
165
- kind: "string",
166
- required: false,
167
- minLength: 1
168
- }]
75
+ parameters: []
169
76
  }]
170
77
  },
171
- state: modeState,
172
- async handleAction(_runtime, actionId, argumentsValue, { signal }) {
78
+ state: () => ({
79
+ activation: modeSelected() ? "active" : "inactive",
80
+ condition: "ready",
81
+ ...modeSelected() ? {
82
+ detail: `${launchers.listInstances().length} loops`,
83
+ color: "accent"
84
+ } : {},
85
+ actions: [{
86
+ id: "activate",
87
+ enabled: !modeSelected(),
88
+ ...modeSelected() ? { disabledReason: "Loop mode is already active." } : {}
89
+ }, {
90
+ id: "deactivate",
91
+ enabled: modeSelected(),
92
+ ...modeSelected() ? {} : { disabledReason: "Loop mode is not active." }
93
+ }]
94
+ }),
95
+ async handleAction(_runtime, actionId, _arguments, { signal }) {
173
96
  signal.throwIfAborted();
174
- if (actionId === "start" && !launchers) {
175
- const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== MODE_ID);
176
- await host.changeSelection({
177
- axis: "state",
178
- key: "minor-mode",
179
- values: [...modes, MODE_ID]
180
- });
181
- }
182
- if (!launchers) throw new Error("Loop activity is not active.");
183
- if (actionId === "start") {
184
- const instance = await launchers.launch(String(argumentsValue.launcherId ?? ""), argumentsValue.instanceId ? { instanceId: String(argumentsValue.instanceId) } : {});
185
- publishMode();
186
- return { message: instance ? `Loop '${instance.instanceId}' started.` : "Loop launch was cancelled." };
187
- }
188
- if (actionId === "stop") {
189
- const stopped = await launchers.stop(String(argumentsValue.instanceId ?? ""), String(argumentsValue.reason ?? "Stopped through minor_mode."));
190
- if (stopped && launchers.listInstances().length === 0) {
191
- const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== MODE_ID);
192
- await host.changeSelection({
193
- axis: "state",
194
- key: "minor-mode",
195
- values: modes
97
+ if (actionId !== "activate" && actionId !== "deactivate") throw new Error(`Unknown loop mode action: ${actionId}`);
98
+ const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== MODE_STATUS_ID);
99
+ await host.changeSelection({
100
+ axis: "state",
101
+ key: "minor-mode",
102
+ values: actionId === "activate" ? [...modes, MODE_STATUS_ID] : modes
103
+ });
104
+ modeOwner.publish();
105
+ return { message: actionId === "activate" ? "Loop tools activated." : "Loop tools deactivated. Existing loops are unchanged." };
106
+ }
107
+ }).createOwner(void 0);
108
+ const publish = () => {
109
+ if (disposed) return;
110
+ modeOwner.publish();
111
+ publishView();
112
+ };
113
+ const unsubscribe = launchers.subscribe(publish);
114
+ let submitting = false;
115
+ for (const cron of [false, true]) launchers.register({
116
+ id: cron ? "doompi.cron" : "doompi.default",
117
+ source: PACKAGE_SOURCE,
118
+ label: cron ? "Cron loop" : "Default loop",
119
+ description: cron ? "Run a prompt on a five-field cron schedule." : "Run a prompt in this session on an interval.",
120
+ inputSchema: { ...cron ? CronLoopSchema : IntervalLoopSchema },
121
+ async launch(request) {
122
+ const execution = host.context;
123
+ const prepared = await prepareLoop(execution, request, cron);
124
+ if (!prepared || request.signal.aborted) return void 0;
125
+ let stopped = false;
126
+ const tick = async () => {
127
+ if (stopped || request.signal.aborted || submitting) return;
128
+ submitting = true;
129
+ try {
130
+ const activity = await execution.session.activity();
131
+ if (stopped || request.signal.aborted || !activity.isIdle || activity.hasPendingMessages) return;
132
+ await execution.session.prompt(prepared.prompt);
133
+ } catch (error) {
134
+ await execution.client.notify({
135
+ body: `Loop pass failed: ${error instanceof Error ? error.message : String(error)}`,
136
+ level: "warning"
196
137
  });
138
+ } finally {
139
+ submitting = false;
140
+ }
141
+ };
142
+ let stopTimer;
143
+ let detail;
144
+ if ("cron" in prepared) {
145
+ const job = new Cron(prepared.cron, {
146
+ timezone: prepared.timezone,
147
+ unref: true
148
+ }, tick);
149
+ if (job.nextRun() === null) {
150
+ job.stop();
151
+ throw new Error("Cron expression has no future run.");
197
152
  }
198
- publishMode();
199
- return { message: stopped ? "Loop stopped." : "Loop instance was not active." };
153
+ stopTimer = () => {
154
+ job.stop();
155
+ };
156
+ detail = `${prepared.cron} (${prepared.timezone})`;
157
+ } else {
158
+ const timer = setInterval(() => {
159
+ tick();
160
+ }, prepared.intervalSeconds * 1e3);
161
+ timer.unref?.();
162
+ stopTimer = () => clearInterval(timer);
163
+ detail = `every ${prepared.intervalSeconds}s`;
164
+ tick();
200
165
  }
201
- throw new Error(`Unknown loop mode action: ${actionId}`);
166
+ return {
167
+ instanceId: request.instanceId,
168
+ label: cron ? "Cron loop" : "Default loop",
169
+ detail,
170
+ stop() {
171
+ stopped = true;
172
+ stopTimer();
173
+ }
174
+ };
202
175
  }
203
- }).createOwner(void 0);
176
+ });
204
177
  return {
205
- services: [serverMinorModes([modeOwner])],
178
+ loopTools: createLoopTools(() => launchers, () => {
179
+ if (!modeSelected()) throw new Error("Enable Loop minor mode before using agent loop tools.");
180
+ }).map((tool) => ({
181
+ ...tool,
182
+ when: {
183
+ state: { "minor-mode": MODE_STATUS_ID },
184
+ attribution: {
185
+ kind: "minor",
186
+ mode: MODE_STATUS_ID
187
+ }
188
+ }
189
+ })),
190
+ services: [(context) => {
191
+ context.plugin((provider) => {
192
+ provider.provide(DOOM_LOOP_LAUNCHERS_SERVICE, launchers);
193
+ });
194
+ context.effect(() => host.subscribeSelection(publish));
195
+ }, serverMinorModes([modeOwner])],
206
196
  resources: [{
207
197
  when: {
208
- state: { "minor-mode": MODE_ID },
198
+ state: { "minor-mode": MODE_STATUS_ID },
209
199
  attribution: {
210
200
  kind: "minor",
211
- mode: MODE_ID
201
+ mode: MODE_STATUS_ID
212
202
  }
213
203
  },
214
204
  name: "doompi-use-loop",
@@ -216,29 +206,19 @@ function createSessionState(host) {
216
206
  read: () => readPackageResource(import.meta.url, "src/prompts/doompi-use-loop/SKILL.md")
217
207
  }],
218
208
  activities: [{
219
- when: {
220
- state: { "minor-mode": MODE_ID },
221
- attribution: {
222
- kind: "minor",
223
- mode: MODE_ID
224
- }
225
- },
226
209
  name: "doompi-loop",
227
- start: startActivity
210
+ async start() {
211
+ publish();
212
+ return async () => {
213
+ await launchers.stopAll("Session loop activity stopped.");
214
+ publish();
215
+ };
216
+ }
228
217
  }],
229
218
  commands: [{
230
219
  name: START_COMMAND_NAME,
231
- description: "Start a registered loop.",
220
+ description: "Start a registered loop manually.",
232
221
  async execute(args, execution) {
233
- if (!launchers) {
234
- const modes = host.context.selection.state?.["minor-mode"] ?? [];
235
- await host.changeSelection({
236
- axis: "state",
237
- key: "minor-mode",
238
- values: [...modes, MODE_ID]
239
- });
240
- }
241
- if (!launchers) throw new Error("Loop activity is not active.");
242
222
  const available = launchers.listLaunchers();
243
223
  const selected = args.trim() || await execution.client.request({
244
224
  kind: "select",
@@ -248,47 +228,57 @@ function createSessionState(host) {
248
228
  value: entry.id
249
229
  }))
250
230
  });
251
- const launcherId = args.trim() || (typeof selected === "string" ? selected : "");
252
- if (!launcherId) return;
253
- const instance = await launchers.launch(launcherId);
254
- publishMode();
255
- await notify(execution, instance ? `Loop '${instance.instanceId}' started.` : "Loop launch was cancelled.");
231
+ if (typeof selected !== "string" || !selected) return;
232
+ const instance = await launchers.launch(selected);
233
+ await execution.client.notify({
234
+ body: instance ? `Loop '${instance.instanceId}' started.` : "Loop launch was cancelled.",
235
+ level: "info"
236
+ });
256
237
  }
257
238
  }, {
258
239
  name: LIST_COMMAND_NAME,
259
- description: "List and stop active loops.",
260
- async execute(_args, execution) {
261
- const instances = launchers?.listInstances() ?? [];
262
- await notify(execution, instances.length === 0 ? "No loops are active." : JSON.stringify(instances, null, 2));
240
+ description: "List or stop session loops. Usage: /loops [stop instanceId].",
241
+ async execute(args, execution) {
242
+ const parts = args.trim().split(/\s+/u);
243
+ if (parts[0] === "stop" && parts.length === 2) {
244
+ const stopped = await launchers.stop(parts[1], "Stopped manually.");
245
+ await execution.client.notify({
246
+ body: stopped ? "Loop stopped." : "Loop instance was not active.",
247
+ level: "info"
248
+ });
249
+ return;
250
+ }
251
+ if (args.trim()) throw new Error("Usage: /loops [stop instanceId]");
252
+ const instances = launchers.listInstances();
253
+ await execution.client.notify({
254
+ body: instances.length === 0 ? "No loops are active." : JSON.stringify(instances, null, 2),
255
+ level: "info"
256
+ });
263
257
  }
264
258
  }],
265
259
  hooks: [{
266
260
  event: "session_start",
267
261
  async handle() {
268
- publishView();
262
+ publish();
269
263
  }
270
264
  }, {
271
- when: {
272
- state: { "minor-mode": MODE_ID },
273
- attribution: {
274
- kind: "minor",
275
- mode: MODE_ID
276
- }
277
- },
278
265
  event: "session_shutdown",
279
266
  async handle() {
280
- await launchers?.stopAll("Headless session shutdown.");
267
+ await launchers.stopAll("Headless session shutdown.");
281
268
  }
282
269
  }],
270
+ onStart: publish,
283
271
  async onDispose() {
284
- await defaultRegistration?.dispose("Headless loop facet disposed.");
285
- await launchers?.dispose("Headless loop facet disposed.");
272
+ if (disposed) return;
273
+ disposed = true;
274
+ unsubscribe();
275
+ await launchers.dispose("Headless loop facet disposed.");
286
276
  }
287
277
  };
288
278
  }
289
279
  //#endregion
290
280
  //#region src/extensions/workspaces/sessions/(backend)/_lib/runtime.server.ts
291
- var runtime_server_default = (({ agent }) => agent ? createSessionState(agent) : {});
281
+ var runtime_server_default = (({ agent }) => agent ? createSessionState(agent) : { loopTools: [] });
292
282
  //#endregion
293
283
  //#region src/extensions/workspaces/sessions/(backend)/root.server.ts
294
284
  var root_server_default = defineRoot((context) => {
@@ -318,14 +308,23 @@ var session_start_server_default = defineRoutedContribution((context) => context
318
308
  //#region src/extensions/workspaces/sessions/(backend)/resource/doompi-use-loop.server.ts
319
309
  var doompi_use_loop_server_default = defineRoutedContribution((context) => context.root.resources?.[0], { cardinality: "optional" });
320
310
  //#endregion
311
+ //#region src/extensions/workspaces/sessions/(backend)/tool/loop-list.server.ts
312
+ var loop_list_server_default = defineTool((context) => context.root.loopTools[0]);
313
+ //#endregion
314
+ //#region src/extensions/workspaces/sessions/(backend)/tool/loop-start.server.ts
315
+ var loop_start_server_default = defineTool((context) => context.root.loopTools[1]);
316
+ //#endregion
317
+ //#region src/extensions/workspaces/sessions/(backend)/tool/loop-stop.server.ts
318
+ var loop_stop_server_default = defineTool((context) => context.root.loopTools[2]);
319
+ //#endregion
321
320
  //#region generated/server.ts
322
321
  const at = (value, context) => typeof value === "function" ? value(context) : value;
323
322
  /* v8 ignore next -- generated normalization branch */
324
323
  const defined = (values) => values.filter((value) => value !== void 0);
325
- const via = (identity, value) => ({
324
+ const via = (identity, value) => value === void 0 ? void 0 : {
326
325
  ...identity,
327
326
  ...value
328
- });
327
+ };
329
328
  const facet = defineServerPlugin({
330
329
  name: "@agimon-ai/doompi-loop",
331
330
  session: async (context) => {
@@ -345,6 +344,13 @@ const facet = defineServerPlugin({
345
344
  get resources() {
346
345
  return defined([at(doompi_use_loop_server_default, contextSession)]);
347
346
  },
347
+ get tools() {
348
+ return [
349
+ via({ name: "loop_list" }, at(loop_list_server_default, contextSession)),
350
+ via({ name: "loop_start" }, at(loop_start_server_default, contextSession)),
351
+ via({ name: "loop_stop" }, at(loop_stop_server_default, contextSession))
352
+ ];
353
+ },
348
354
  services: [...scopeSession.services ?? []],
349
355
  get activities() {
350
356
  return [...scopeSession.activities ?? []];