loopengine 0.1.68 → 0.1.70
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 +33 -3
- package/dist/core/agent-config.d.ts +8 -0
- package/dist/core/http-notifier.js +2 -2
- package/dist/core/http-notify-triggers/lark.d.ts +9 -0
- package/dist/core/http-notify-triggers/lark.js +39 -19
- package/dist/core/notifier-file.d.ts +4 -0
- package/dist/core/notifier-file.js +17 -4
- package/dist/core/run-agent.js +7 -2
- package/dist/core/scheduler.d.ts +143 -0
- package/dist/core/scheduler.js +359 -0
- package/dist/core/slack-interactions.d.ts +59 -0
- package/dist/core/slack-interactions.js +168 -0
- package/dist/core/system-tools/index.d.ts +1 -0
- package/dist/core/system-tools/index.js +1 -0
- package/dist/core/system-tools/schedule.d.ts +2 -0
- package/dist/core/system-tools/schedule.js +111 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/web/agents-config-page.js +225 -4
- package/dist/web/console-shell.js +1 -1
- package/dist/web/notifier-admin.d.ts +25 -1
- package/dist/web/notifier-admin.js +77 -2
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -121,6 +121,7 @@ code changes or redeploy needed for most of it:
|
|
|
121
121
|
| **Tools** | Your hand-written tools, tools from integrations (e.g. [Composio](https://composio.dev)) and subagents, with whether each call runs on its own, asks first, or is denied. Build an HTTP tool or connect an integration without touching a file. |
|
|
122
122
|
| **Skills** | Create, edit, and delete `SKILL.md` files for this agent directly in the browser — write the body, preview the rendered markdown, save. |
|
|
123
123
|
| **Permissions** | Add, edit, and delete permission rules (actauth) — who, tool, decision — and choose what happens when no rule matches. |
|
|
124
|
+
| **Schedules** | Messages the agent receives on a timer — once at a set time, or every N minutes/hours/days. Run now, pause, resume, cancel, see each run's result, and optionally send results through the agent's notifier. |
|
|
124
125
|
| **Environment** | Every setting an installed ability reads, with its project-wide value and this agent's own value side by side, and which one the agent actually uses. A secret is never shown again once set. |
|
|
125
126
|
|
|
126
127
|
Models and Integrations in the sidebar show which model providers have a
|
|
@@ -182,9 +183,14 @@ waiting with Approve/Deny buttons. The tab warns you when an agent has
|
|
|
182
183
|
Every notification links to a **decision page**: a signed, single-use
|
|
183
184
|
link (`/decide/<token>`, valid 7 days) where whoever got the message can
|
|
184
185
|
approve, deny, edit the arguments or answer a question, with no login and
|
|
185
|
-
no app of your own. So the quickest setup is Slack:
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
no app of your own. So the quickest setup is Slack: create the Slack app
|
|
187
|
+
from the manifest the console shows, paste its bot token, pick a channel
|
|
188
|
+
from the list, save; each approval arrives with an "Open to approve or
|
|
189
|
+
deny" link. Add the app's signing secret too and the message also gets
|
|
190
|
+
Approve/Deny buttons, handled by LoopEngine itself at
|
|
191
|
+
`/hooks/slack/<agent>`, with no handler to deploy. Lark cards carry the
|
|
192
|
+
same link. Links and buttons need the server to be reachable from where
|
|
193
|
+
people click (a public HTTPS address or a tunnel).
|
|
188
194
|
|
|
189
195
|
For a third-party app calling an agent over plain HTTP:
|
|
190
196
|
|
|
@@ -206,6 +212,30 @@ click, a webhook HMAC signature, a magic-link token) ship under
|
|
|
206
212
|
live and durable, worked examples, and how resumption actually works
|
|
207
213
|
under the hood.
|
|
208
214
|
|
|
215
|
+
## Scheduled tasks
|
|
216
|
+
|
|
217
|
+
The server runs one scheduler (`core/scheduler.ts`) that sends a message
|
|
218
|
+
to an agent later — once (`run_at`) or on an interval (`every: "1h"`) —
|
|
219
|
+
from the console's **Schedules** tab, the `GET/POST
|
|
220
|
+
/agents/<name>/schedules` API, or the agent itself: every agent has
|
|
221
|
+
built-in `system_schedule_task`, `system_list_scheduled_tasks`,
|
|
222
|
+
`system_check_scheduled_task` and `system_cancel_scheduled_task` tools,
|
|
223
|
+
so "remind me tomorrow at 9" just works. An
|
|
224
|
+
agent can only schedule, see or cancel its own tasks, up to 25 active at
|
|
225
|
+
a time (`LOOPENGINE_SCHEDULER_MAX_PER_AGENT`); `selfScheduling: false` in
|
|
226
|
+
its config removes the tools. Tasks left by the old `lp-task-scheduler`
|
|
227
|
+
ability are imported at start. Each run goes through the same
|
|
228
|
+
path as an HTTP call: an approval goes wherever the Permissions tab sends
|
|
229
|
+
it (and resumes the run once decided); with nowhere set, a call that
|
|
230
|
+
needs approval is denied. Intervals keep their original slot (an hourly
|
|
231
|
+
task created at 10:07 stays on :07); a run missed while the server was
|
|
232
|
+
down fires once when it's back. Tasks live in `.scheduler/` (or
|
|
233
|
+
`LOOPENGINE_SCHEDULER_DIR`); the tick is `LOOPENGINE_SCHEDULER_TICK_MS`
|
|
234
|
+
(10 s); `LOOPENGINE_SCHEDULER=off` turns the loop off for this process.
|
|
235
|
+
Behind a hosting platform, `GET /internal/activity` reports `nextWakeAt`
|
|
236
|
+
and `POST /internal/scheduler/tick` fires whatever is due, so a stopped
|
|
237
|
+
server can be woken in time.
|
|
238
|
+
|
|
209
239
|
## Ability system
|
|
210
240
|
|
|
211
241
|
Giving an agent a new capability is usually three separate, hand-authored
|
|
@@ -203,6 +203,8 @@ export type HttpNotifierConfig = {
|
|
|
203
203
|
appId: string;
|
|
204
204
|
appSecret: string;
|
|
205
205
|
chatId: string;
|
|
206
|
+
publicUrl?: string;
|
|
207
|
+
buttons?: boolean;
|
|
206
208
|
};
|
|
207
209
|
events: HttpNotifierEvent[];
|
|
208
210
|
} | {
|
|
@@ -397,6 +399,12 @@ export interface AgentConfig {
|
|
|
397
399
|
* for `cli`/`http_stream`: both always get the library's own live
|
|
398
400
|
* defaults, with nothing to configure. */
|
|
399
401
|
httpNotifier?: HttpNotifierConfig;
|
|
402
|
+
/** Whether this agent gets the built-in scheduling tools
|
|
403
|
+
* (system_schedule_task and friends, core/system-tools/schedule.ts),
|
|
404
|
+
* which let it schedule messages to itself — reminders, recurring
|
|
405
|
+
* checks — and list or cancel its own scheduled tasks. Default true;
|
|
406
|
+
* false leaves them out of this agent's tools entirely. */
|
|
407
|
+
selfScheduling?: boolean;
|
|
400
408
|
/** Fires once, fire-and-forget, at the start of every `runAgent()`/
|
|
401
409
|
* `resumeAgent()` call on the `http` channel (or no channel at all —
|
|
402
410
|
* see below) — the "someone triggered this agent" signal, for
|
|
@@ -114,8 +114,8 @@ function buildResolved(httpNotifier) {
|
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
116
|
case 'lark': {
|
|
117
|
-
const { appId, appSecret, chatId } = httpNotifier.config;
|
|
118
|
-
const lark = new LarkNotifier({ appId, appSecret, chatId });
|
|
117
|
+
const { appId, appSecret, chatId, publicUrl, buttons } = httpNotifier.config;
|
|
118
|
+
const lark = new LarkNotifier({ appId, appSecret, chatId, publicUrl, buttons });
|
|
119
119
|
resolved = {
|
|
120
120
|
approver: events.has('approval') ? lark : undefined,
|
|
121
121
|
questionHandler: events.has('question') ? lark : undefined,
|
|
@@ -9,11 +9,20 @@ export declare class LarkNotifier implements DurableApprover, DurableQuestionHan
|
|
|
9
9
|
private readonly appId;
|
|
10
10
|
private readonly appSecret;
|
|
11
11
|
private readonly chatId;
|
|
12
|
+
private readonly publicUrl?;
|
|
13
|
+
private readonly buttons;
|
|
14
|
+
/** `publicUrl` / `buttons`: same meaning as SlackNotifier's — a decision
|
|
15
|
+
* link in each card when the server's address is known, and whether to
|
|
16
|
+
* add card buttons, which only work with a card-callback handler
|
|
17
|
+
* deployed (examples/notifier-handler/lark.ts). */
|
|
12
18
|
constructor(options: {
|
|
13
19
|
appId: string;
|
|
14
20
|
appSecret: string;
|
|
15
21
|
chatId: string;
|
|
22
|
+
publicUrl?: string;
|
|
23
|
+
buttons?: boolean;
|
|
16
24
|
});
|
|
25
|
+
private decideLine;
|
|
17
26
|
requestDurableApproval(tool: string, args: Record<string, unknown>, scope: Scope, reason: string): {
|
|
18
27
|
pendingId: string;
|
|
19
28
|
};
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
// sending and receiving ever run in different processes), deliberately
|
|
39
39
|
// not the approach used here.
|
|
40
40
|
import { randomUUID } from 'node:crypto';
|
|
41
|
+
import { decidePath } from '../approval-links.js';
|
|
42
|
+
import { publicBaseUrl } from './webhook.js';
|
|
41
43
|
/** Fixed id for the question form card's own input field — used both
|
|
42
44
|
* when *building* the card (notifyPendingQuestion below) and when
|
|
43
45
|
* *reading* the human's answer back out of the callback payload (the
|
|
@@ -62,10 +64,22 @@ export class LarkNotifier {
|
|
|
62
64
|
appId;
|
|
63
65
|
appSecret;
|
|
64
66
|
chatId;
|
|
67
|
+
publicUrl;
|
|
68
|
+
buttons;
|
|
69
|
+
/** `publicUrl` / `buttons`: same meaning as SlackNotifier's — a decision
|
|
70
|
+
* link in each card when the server's address is known, and whether to
|
|
71
|
+
* add card buttons, which only work with a card-callback handler
|
|
72
|
+
* deployed (examples/notifier-handler/lark.ts). */
|
|
65
73
|
constructor(options) {
|
|
66
74
|
this.appId = options.appId;
|
|
67
75
|
this.appSecret = options.appSecret;
|
|
68
76
|
this.chatId = options.chatId;
|
|
77
|
+
this.publicUrl = options.publicUrl;
|
|
78
|
+
this.buttons = options.buttons ?? true;
|
|
79
|
+
}
|
|
80
|
+
decideLine(pendingId, kind) {
|
|
81
|
+
const base = publicBaseUrl(this.publicUrl);
|
|
82
|
+
return base ? `\n[${kind === 'approval' ? 'Open to approve or deny' : 'Open to answer'}](${base}${decidePath(pendingId, kind)})` : '';
|
|
69
83
|
}
|
|
70
84
|
requestDurableApproval(tool, args, scope, reason) {
|
|
71
85
|
const pendingId = randomUUID();
|
|
@@ -80,16 +94,18 @@ export class LarkNotifier {
|
|
|
80
94
|
tag: 'div',
|
|
81
95
|
text: {
|
|
82
96
|
tag: 'lark_md',
|
|
83
|
-
content: `**tool:** ${tool}\n**args:** \`${JSON.stringify(args)}\`\n**scope:** ${scope.tenant}/${scope.environment}/${scope.agent}\n**reason:** ${reason}`,
|
|
97
|
+
content: `**tool:** ${tool}\n**args:** \`${JSON.stringify(args)}\`\n**scope:** ${scope.tenant}/${scope.environment}/${scope.agent}\n**reason:** ${reason}${this.decideLine(pendingId, 'approval')}`,
|
|
84
98
|
},
|
|
85
99
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
...(this.buttons
|
|
101
|
+
? [{
|
|
102
|
+
tag: 'action',
|
|
103
|
+
actions: [
|
|
104
|
+
{ tag: 'button', text: { tag: 'plain_text', content: 'Approve' }, type: 'primary', value: { pendingId, decision: 'approve' } },
|
|
105
|
+
{ tag: 'button', text: { tag: 'plain_text', content: 'Deny' }, type: 'danger', value: { pendingId, decision: 'deny' } },
|
|
106
|
+
],
|
|
107
|
+
}]
|
|
108
|
+
: []),
|
|
93
109
|
],
|
|
94
110
|
};
|
|
95
111
|
await this.sendCard(token, card, pendingId);
|
|
@@ -114,19 +130,23 @@ export class LarkNotifier {
|
|
|
114
130
|
config: { wide_screen_mode: true },
|
|
115
131
|
header: { title: { tag: 'plain_text', content: `Question from ${agent}` } },
|
|
116
132
|
elements: [
|
|
117
|
-
{ tag: 'div', text: { tag: 'lark_md', content: question + (sessionId ? `\n_(session ${sessionId})_` : '') } },
|
|
118
|
-
...(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
name: 'answer_form',
|
|
122
|
-
elements: [
|
|
123
|
-
{ tag: 'input', name: LARK_ANSWER_FIELD_TAG, placeholder: { tag: 'plain_text', content: 'Or type your own answer…' } },
|
|
133
|
+
{ tag: 'div', text: { tag: 'lark_md', content: question + (sessionId ? `\n_(session ${sessionId})_` : '') + this.decideLine(pendingId, 'question') } },
|
|
134
|
+
...(this.buttons
|
|
135
|
+
? [
|
|
136
|
+
...(optionButtons.length ? [{ tag: 'action', actions: optionButtons }] : []),
|
|
124
137
|
{
|
|
125
|
-
tag: '
|
|
126
|
-
|
|
138
|
+
tag: 'form',
|
|
139
|
+
name: 'answer_form',
|
|
140
|
+
elements: [
|
|
141
|
+
{ tag: 'input', name: LARK_ANSWER_FIELD_TAG, placeholder: { tag: 'plain_text', content: 'Or type your own answer…' } },
|
|
142
|
+
{
|
|
143
|
+
tag: 'action',
|
|
144
|
+
actions: [{ tag: 'button', text: { tag: 'plain_text', content: 'Send' }, type: 'primary', action_type: 'form_submit', value: { pendingId } }],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
127
147
|
},
|
|
128
|
-
]
|
|
129
|
-
|
|
148
|
+
]
|
|
149
|
+
: []),
|
|
130
150
|
],
|
|
131
151
|
};
|
|
132
152
|
await this.sendCard(token, card, pendingId);
|
|
@@ -17,6 +17,10 @@ export interface NotifierFile {
|
|
|
17
17
|
}
|
|
18
18
|
/** The env var each channel's secret is read from, per agent. */
|
|
19
19
|
export declare const NOTIFIER_SECRET_VARS: Record<NotifierFileChannel, string>;
|
|
20
|
+
/** Optional: a Slack app's signing secret, per agent. Set, it turns on
|
|
21
|
+
* Approve/Deny buttons in Slack, handled by the server itself at
|
|
22
|
+
* POST /hooks/slack/<agent>; unset, messages carry only the decision link. */
|
|
23
|
+
export declare const NOTIFIER_SLACK_SIGNING_SECRET_VAR = "LOOPENGINE_NOTIFIER_SLACK_SIGNING_SECRET";
|
|
20
24
|
export declare class NotifierFileError extends Error {
|
|
21
25
|
}
|
|
22
26
|
export declare function notifierFilePath(dir: string): string;
|
|
@@ -17,6 +17,10 @@ export const NOTIFIER_SECRET_VARS = {
|
|
|
17
17
|
slack: 'LOOPENGINE_NOTIFIER_SLACK_BOT_TOKEN',
|
|
18
18
|
lark: 'LOOPENGINE_NOTIFIER_LARK_APP_SECRET',
|
|
19
19
|
};
|
|
20
|
+
/** Optional: a Slack app's signing secret, per agent. Set, it turns on
|
|
21
|
+
* Approve/Deny buttons in Slack, handled by the server itself at
|
|
22
|
+
* POST /hooks/slack/<agent>; unset, messages carry only the decision link. */
|
|
23
|
+
export const NOTIFIER_SLACK_SIGNING_SECRET_VAR = 'LOOPENGINE_NOTIFIER_SLACK_SIGNING_SECRET';
|
|
20
24
|
const CHANNELS = ['webhook', 'slack', 'lark'];
|
|
21
25
|
const EVENTS = ['approval', 'question', 'run_start', 'run_finish'];
|
|
22
26
|
export class NotifierFileError extends Error {
|
|
@@ -95,10 +99,19 @@ export function notifierConfigFromFile(file, env) {
|
|
|
95
99
|
case 'webhook':
|
|
96
100
|
return { config: { channel: 'webhook', config: { webhookUrl: file.url, webhookSecret: secret, publicUrl: file.publicUrl }, events: file.events } };
|
|
97
101
|
case 'slack':
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
102
|
+
// Buttons only once the agent has a signing secret, i.e. the Slack
|
|
103
|
+
// app's interactivity points at /hooks/slack/<agent>; otherwise the
|
|
104
|
+
// decision link does the job.
|
|
105
|
+
return {
|
|
106
|
+
config: {
|
|
107
|
+
channel: 'slack',
|
|
108
|
+
config: { botToken: secret, channelId: file.channelId, publicUrl: file.publicUrl, buttons: Boolean(env.get(NOTIFIER_SLACK_SIGNING_SECRET_VAR)) },
|
|
109
|
+
events: file.events,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
101
112
|
case 'lark':
|
|
102
|
-
|
|
113
|
+
// No card buttons: nothing built in handles Lark's card callbacks yet,
|
|
114
|
+
// so a console-configured Lark notifier relies on the decision link.
|
|
115
|
+
return { config: { channel: 'lark', config: { appId: file.appId, appSecret: secret, chatId: file.chatId, publicUrl: file.publicUrl, buttons: false }, events: file.events } };
|
|
103
116
|
}
|
|
104
117
|
}
|
package/dist/core/run-agent.js
CHANGED
|
@@ -19,7 +19,7 @@ import { recordUsage } from './usage.js';
|
|
|
19
19
|
import { loadAgentModule } from './discover-agents.js';
|
|
20
20
|
import { agentAsTool } from './agent-as-tool.js';
|
|
21
21
|
import { loadGatewayToolsFromDir } from './gateway-tools.js';
|
|
22
|
-
import { systemTools, createAskUserTool, CliQuestionHandler, isDurableQuestionHandler } from './system-tools/index.js';
|
|
22
|
+
import { systemTools, createAskUserTool, createScheduleTools, CliQuestionHandler, isDurableQuestionHandler } from './system-tools/index.js';
|
|
23
23
|
import { resolveHttpNotifier } from './http-notifier.js';
|
|
24
24
|
import { CRASH_RECOVERY_CONTINUATION } from './session-store.js';
|
|
25
25
|
export { systemTools } from './system-tools/index.js';
|
|
@@ -296,9 +296,14 @@ async function buildTurnContext(config, modelCall, options) {
|
|
|
296
296
|
// (options.onEvent's own fallback) — see RunAgentOptions.onQuestionPending's
|
|
297
297
|
// own doc comment for why those two are deliberately not the same thing.
|
|
298
298
|
const askUserTool = createAskUserTool({ agent: config.name, sessionId: options.sessionId }, options.onQuestionPending);
|
|
299
|
+
// Bound to this agent: it can schedule, list and cancel only its own
|
|
300
|
+
// tasks (see core/system-tools/schedule.ts). AgentConfig.selfScheduling
|
|
301
|
+
// false leaves them out.
|
|
302
|
+
const scheduleTools = config.selfScheduling === false ? [] : createScheduleTools(config.name);
|
|
299
303
|
const tools = dedupeToolsByName([
|
|
300
304
|
...systemTools,
|
|
301
305
|
askUserTool,
|
|
306
|
+
...scheduleTools,
|
|
302
307
|
...(config.tools ?? (await loadDefaultTools(config))),
|
|
303
308
|
...(await loadSubagentAsTools(config)),
|
|
304
309
|
...(await loadGatewayToolsFromDir(join(process.cwd(), 'agents', config.name))),
|
|
@@ -390,7 +395,7 @@ async function buildTurnContext(config, modelCall, options) {
|
|
|
390
395
|
// ('ask' or 'deny', see loadRules' own doc comment) would otherwise
|
|
391
396
|
// catch it, since neither system tool ever appears in an agent's own
|
|
392
397
|
// actauth.yml.
|
|
393
|
-
const systemToolInstances = new Set([...systemTools, askUserTool]);
|
|
398
|
+
const systemToolInstances = new Set([...systemTools, askUserTool, ...scheduleTools]);
|
|
394
399
|
const toolSchemas = tools.map(({ name, description, input_schema }) => ({
|
|
395
400
|
name,
|
|
396
401
|
description,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export type Schedule = {
|
|
2
|
+
run_at: string;
|
|
3
|
+
} | {
|
|
4
|
+
every: string;
|
|
5
|
+
anchor?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ScheduledTaskStatus = 'scheduled' | 'running' | 'awaiting_approval' | 'paused' | 'done' | 'cancelled';
|
|
8
|
+
export interface ScheduledTaskRun {
|
|
9
|
+
ran_at: string;
|
|
10
|
+
status: 'ok' | 'error';
|
|
11
|
+
result?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
session_id?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ScheduledTask {
|
|
16
|
+
task_id: string;
|
|
17
|
+
task_name?: string;
|
|
18
|
+
agent: string;
|
|
19
|
+
message: string;
|
|
20
|
+
schedule: Schedule;
|
|
21
|
+
status: ScheduledTaskStatus;
|
|
22
|
+
created_at: string;
|
|
23
|
+
next_run_at?: string;
|
|
24
|
+
run_count: number;
|
|
25
|
+
last_run_at?: string;
|
|
26
|
+
last_status?: 'ok' | 'error';
|
|
27
|
+
last_result?: string;
|
|
28
|
+
last_error?: string;
|
|
29
|
+
/** Set while a run is paused on an approval or question. */
|
|
30
|
+
pending?: {
|
|
31
|
+
ran_at: string;
|
|
32
|
+
session_id: string;
|
|
33
|
+
pending_ids: string[];
|
|
34
|
+
};
|
|
35
|
+
/** Send each run's result through the agent's notifier. */
|
|
36
|
+
notify?: boolean;
|
|
37
|
+
history: ScheduledTaskRun[];
|
|
38
|
+
/** Set by "run now" on a paused or finished task, so it goes back to
|
|
39
|
+
* that status after the extra run. */
|
|
40
|
+
run_now_from?: 'paused' | 'done';
|
|
41
|
+
/** Set by cancelling a run that's waiting on an approval: cancelled
|
|
42
|
+
* once that run finishes. */
|
|
43
|
+
cancel_after_run?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class ScheduleError extends Error {
|
|
46
|
+
}
|
|
47
|
+
export declare function parseEvery(every: string): number;
|
|
48
|
+
/** Human-readable schedule, for lists and messages. */
|
|
49
|
+
export declare function describeSchedule(schedule: Schedule): string;
|
|
50
|
+
export interface SchedulerStore {
|
|
51
|
+
list(): Promise<ScheduledTask[]>;
|
|
52
|
+
get(taskId: string): Promise<ScheduledTask | undefined>;
|
|
53
|
+
put(task: ScheduledTask): Promise<void>;
|
|
54
|
+
remove(taskId: string): Promise<void>;
|
|
55
|
+
/** Runs `fn` with exclusive access to one task (read, change, write). */
|
|
56
|
+
update(taskId: string, fn: (task: ScheduledTask | undefined) => ScheduledTask | undefined): Promise<ScheduledTask | undefined>;
|
|
57
|
+
}
|
|
58
|
+
/** One JSON file per task under `<dir>/tasks/` — one server process. */
|
|
59
|
+
export declare class FileSchedulerStore implements SchedulerStore {
|
|
60
|
+
private readonly dir;
|
|
61
|
+
constructor(dir: string);
|
|
62
|
+
private file;
|
|
63
|
+
list(): Promise<ScheduledTask[]>;
|
|
64
|
+
get(taskId: string): Promise<ScheduledTask | undefined>;
|
|
65
|
+
put(task: ScheduledTask): Promise<void>;
|
|
66
|
+
remove(taskId: string): Promise<void>;
|
|
67
|
+
update(taskId: string, fn: (task: ScheduledTask | undefined) => ScheduledTask | undefined): Promise<ScheduledTask | undefined>;
|
|
68
|
+
}
|
|
69
|
+
/** The process-wide store: `.scheduler/` in the project, or
|
|
70
|
+
* LOOPENGINE_SCHEDULER_DIR. */
|
|
71
|
+
export declare function getSchedulerStore(): SchedulerStore;
|
|
72
|
+
export declare function setSchedulerStore(next: SchedulerStore): void;
|
|
73
|
+
export interface CreateScheduledTaskInput {
|
|
74
|
+
agent: string;
|
|
75
|
+
message: string;
|
|
76
|
+
schedule: {
|
|
77
|
+
run_at?: unknown;
|
|
78
|
+
every?: unknown;
|
|
79
|
+
};
|
|
80
|
+
task_name?: string;
|
|
81
|
+
notify?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function createScheduledTask(input: CreateScheduledTaskInput, nowMs?: number): Promise<ScheduledTask>;
|
|
84
|
+
export declare function listScheduledTasks(filter?: {
|
|
85
|
+
agent?: string;
|
|
86
|
+
status?: ScheduledTaskStatus;
|
|
87
|
+
}): Promise<ScheduledTask[]>;
|
|
88
|
+
export declare function getScheduledTask(taskId: string): Promise<ScheduledTask | undefined>;
|
|
89
|
+
/** Stops future runs; history stays. A run already waiting on an
|
|
90
|
+
* approval isn't retracted. */
|
|
91
|
+
export declare function cancelScheduledTask(taskId: string): Promise<ScheduledTask>;
|
|
92
|
+
export declare function pauseScheduledTask(taskId: string): Promise<ScheduledTask>;
|
|
93
|
+
export declare function resumeScheduledTask(taskId: string, nowMs?: number): Promise<ScheduledTask>;
|
|
94
|
+
/** Fires the task on the next tick, without changing its schedule. A
|
|
95
|
+
* finished one-off task runs once more. */
|
|
96
|
+
export declare function runScheduledTaskNow(taskId: string, nowMs?: number): Promise<ScheduledTask>;
|
|
97
|
+
export declare function deleteScheduledTask(taskId: string): Promise<void>;
|
|
98
|
+
/** When the next task is due — for a host that stops idle servers and
|
|
99
|
+
* needs to wake this one in time (GET /internal/activity). */
|
|
100
|
+
export declare function nextScheduledWakeAt(): Promise<string | undefined>;
|
|
101
|
+
/** Moves the old lp-task-scheduler's task files (`<dir>/.tasks/*.json`)
|
|
102
|
+
* into this store once, renaming each to `.json.migrated`. Returns how
|
|
103
|
+
* many it imported. */
|
|
104
|
+
export declare function importLegacyScheduledTasks(dir: string): Promise<number>;
|
|
105
|
+
/** How a task fires — supplied by the server. `done`: the agent replied;
|
|
106
|
+
* `pending`: the run is paused on approvals/questions (see settleFromResume). */
|
|
107
|
+
export type FireTask = (task: ScheduledTask, sessionId: string) => Promise<{
|
|
108
|
+
kind: 'done';
|
|
109
|
+
text: string;
|
|
110
|
+
} | {
|
|
111
|
+
kind: 'pending';
|
|
112
|
+
pendingIds: string[];
|
|
113
|
+
}>;
|
|
114
|
+
export declare class Scheduler {
|
|
115
|
+
private readonly fire;
|
|
116
|
+
private readonly tickMs;
|
|
117
|
+
private readonly onSettled?;
|
|
118
|
+
private readonly inFlight;
|
|
119
|
+
private timer?;
|
|
120
|
+
constructor(options: {
|
|
121
|
+
fire: FireTask;
|
|
122
|
+
tickMs?: number;
|
|
123
|
+
onSettled?: (task: ScheduledTask, run: ScheduledTaskRun) => void;
|
|
124
|
+
});
|
|
125
|
+
/** Imports tasks left by the old lp-task-scheduler ability, recovers
|
|
126
|
+
* runs a restart cut off, then ticks every `tickMs`. */
|
|
127
|
+
start(): Promise<void>;
|
|
128
|
+
stop(): void;
|
|
129
|
+
/** How many runs are executing right now (for idle-shutdown checks). */
|
|
130
|
+
get running(): number;
|
|
131
|
+
/** Fires every task that's due. Safe to call any time (a host's wake-up
|
|
132
|
+
* request calls it directly). */
|
|
133
|
+
tick(nowMs?: number): Promise<number>;
|
|
134
|
+
private run;
|
|
135
|
+
/** A paused run resumed after a decision (adapters/http.ts calls this
|
|
136
|
+
* from the resolve path): settle it, or record the next decision it's
|
|
137
|
+
* now waiting on. */
|
|
138
|
+
settleFromResume(sessionId: string, outcome: {
|
|
139
|
+
text: string;
|
|
140
|
+
pendingIds?: string[];
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
private settle;
|
|
143
|
+
}
|