@tokagent/tokagentos 2.0.24 → 2.0.30
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/package.json +1 -1
- package/scaffold-patches/packages/agent/src/api/plugin-routes.ts +1889 -0
- package/scaffold-patches/packages/agent/src/api/server.ts +4509 -0
- package/scaffold-patches/packages/agent/src/api/trigger-routes.ts +942 -0
- package/scaffold-patches/packages/agent/src/runtime/core-plugins.ts +4 -0
- package/scaffold-patches/packages/agent/src/triggers/runtime.ts +955 -0
- package/scaffold-patches/packages/app-core/src/api/automations-compat-routes.ts +924 -0
- package/scaffold-patches/packages/app-core/src/api/client-agent.ts +2755 -0
- package/scaffold-patches/packages/app-core/src/components/pages/AutomationsView.tsx +446 -26
- package/scaffold-patches/packages/app-core/src/components/pages/SettingsView.tsx +155 -0
- package/scaffold-patches/packages/shared/src/onboarding-presets.characters.ts +16 -16
- package/templates/fullstack-app/package.json +9 -5
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/package.json +1 -1
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/__tests__/routes/estimate-routes.test.ts +5 -2
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/app.js +896 -19
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/index.html +280 -94
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/style.css +969 -235
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/keys-routes.ts +170 -0
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/messages-proxy-routes.ts +114 -3
- package/templates/fullstack-app/plugins/plugin-web-fetch/build.ts +35 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/package.json +37 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/src/index.ts +471 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/tsconfig.json +20 -0
- package/templates/fullstack-app/scripts/ensure-plugin-builds.mjs +1 -0
- package/templates/fullstack-app/scripts/verify-llm-plugins.mjs +122 -0
- package/templates-manifest.json +1 -1
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
Content,
|
|
4
|
+
HandlerCallback,
|
|
5
|
+
IAgentRuntime,
|
|
6
|
+
Memory,
|
|
7
|
+
Service,
|
|
8
|
+
Task,
|
|
9
|
+
UUID,
|
|
10
|
+
} from "@elizaos/core";
|
|
11
|
+
import { EventType, stringToUuid } from "@elizaos/core";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stable, well-known entity ID used as the *sender* of trigger-dispatched
|
|
15
|
+
* instructions. Mirrors the autonomy service's pattern (UUID …0002 for
|
|
16
|
+
* autonomy prompts) so the agent's message pipeline doesn't filter the
|
|
17
|
+
* instruction as "message from self" when entityId === runtime.agentId.
|
|
18
|
+
* The agent's own response is still persisted under runtime.agentId.
|
|
19
|
+
*/
|
|
20
|
+
const TRIGGER_ENTITY_ID = stringToUuid(
|
|
21
|
+
"00000000-0000-0000-0000-000000000003",
|
|
22
|
+
) as UUID;
|
|
23
|
+
import {
|
|
24
|
+
buildTriggerMetadata,
|
|
25
|
+
DISABLED_TRIGGER_INTERVAL_MS,
|
|
26
|
+
MAX_TRIGGER_RUN_HISTORY,
|
|
27
|
+
} from "./scheduling.js";
|
|
28
|
+
import type {
|
|
29
|
+
TriggerConfig,
|
|
30
|
+
TriggerHealthSnapshot,
|
|
31
|
+
TriggerRunRecord,
|
|
32
|
+
TriggerSummary,
|
|
33
|
+
TriggerTaskMetadata,
|
|
34
|
+
} from "./types.js";
|
|
35
|
+
|
|
36
|
+
export const TRIGGER_TASK_NAME = "TRIGGER_DISPATCH" as const;
|
|
37
|
+
export const TRIGGER_TASK_TAGS = ["queue", "repeat", "trigger"] as const;
|
|
38
|
+
const HEARTBEAT_TASK_TAGS = ["queue", "repeat", "heartbeat"] as const;
|
|
39
|
+
|
|
40
|
+
const DEFAULT_MAX_ACTIVE_TRIGGERS = 100;
|
|
41
|
+
|
|
42
|
+
interface TriggerMetricsState {
|
|
43
|
+
totalExecutions: number;
|
|
44
|
+
totalFailures: number;
|
|
45
|
+
totalSkipped: number;
|
|
46
|
+
lastExecutionAt?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TriggerExecutionOptions {
|
|
50
|
+
source: "scheduler" | "manual" | "event";
|
|
51
|
+
force?: boolean;
|
|
52
|
+
event?: {
|
|
53
|
+
kind: string;
|
|
54
|
+
payload?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TriggerExecutionResult {
|
|
59
|
+
status: "success" | "error" | "skipped";
|
|
60
|
+
error?: string;
|
|
61
|
+
taskDeleted: boolean;
|
|
62
|
+
runRecord?: TriggerRunRecord;
|
|
63
|
+
trigger?: TriggerSummary | null;
|
|
64
|
+
// Present when a workflow-kind trigger dispatches to N8N_DISPATCH and
|
|
65
|
+
// the service returns an execution id.
|
|
66
|
+
executionId?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const metricsByAgent = new Map<UUID, TriggerMetricsState>();
|
|
70
|
+
|
|
71
|
+
function getMetrics(agentId: UUID): TriggerMetricsState {
|
|
72
|
+
const current = metricsByAgent.get(agentId);
|
|
73
|
+
if (current) return current;
|
|
74
|
+
const created: TriggerMetricsState = {
|
|
75
|
+
totalExecutions: 0,
|
|
76
|
+
totalFailures: 0,
|
|
77
|
+
totalSkipped: 0,
|
|
78
|
+
};
|
|
79
|
+
metricsByAgent.set(agentId, created);
|
|
80
|
+
return created;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function recordExecutionMetric(
|
|
84
|
+
agentId: UUID,
|
|
85
|
+
status: TriggerExecutionResult["status"],
|
|
86
|
+
ts: number,
|
|
87
|
+
): void {
|
|
88
|
+
const metrics = getMetrics(agentId);
|
|
89
|
+
if (status === "success" || status === "error") {
|
|
90
|
+
metrics.totalExecutions += 1;
|
|
91
|
+
metrics.lastExecutionAt = ts;
|
|
92
|
+
}
|
|
93
|
+
if (status === "error") {
|
|
94
|
+
metrics.totalFailures += 1;
|
|
95
|
+
}
|
|
96
|
+
if (status === "skipped") {
|
|
97
|
+
metrics.totalSkipped += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function appendRunRecord(
|
|
102
|
+
existing: TriggerRunRecord[] | undefined,
|
|
103
|
+
record: TriggerRunRecord,
|
|
104
|
+
): TriggerRunRecord[] {
|
|
105
|
+
const runs = [...(existing ?? []), record];
|
|
106
|
+
return runs.length <= MAX_TRIGGER_RUN_HISTORY
|
|
107
|
+
? runs
|
|
108
|
+
: runs.slice(runs.length - MAX_TRIGGER_RUN_HISTORY);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function taskMetadata(task: Task): TriggerTaskMetadata {
|
|
112
|
+
return (task.metadata ?? {}) as TriggerTaskMetadata;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function readTriggerConfig(task: Task): TriggerConfig | null {
|
|
116
|
+
const trigger = taskMetadata(task).trigger;
|
|
117
|
+
if (!trigger || typeof trigger !== "object" || Array.isArray(trigger))
|
|
118
|
+
return null;
|
|
119
|
+
return (trigger as TriggerConfig).triggerId
|
|
120
|
+
? (trigger as TriggerConfig)
|
|
121
|
+
: null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function readTriggerRuns(task: Task): TriggerRunRecord[] {
|
|
125
|
+
const runs = taskMetadata(task).triggerRuns;
|
|
126
|
+
return Array.isArray(runs) ? (runs as TriggerRunRecord[]) : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function triggersFeatureEnabled(runtime?: IAgentRuntime): boolean {
|
|
130
|
+
const runtimeSetting = runtime?.getSetting("ELIZA_TRIGGERS_ENABLED");
|
|
131
|
+
if (
|
|
132
|
+
runtimeSetting === false ||
|
|
133
|
+
runtimeSetting === "false" ||
|
|
134
|
+
runtimeSetting === "0"
|
|
135
|
+
) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const env = process.env.ELIZA_TRIGGERS_ENABLED;
|
|
139
|
+
if (!env) return true;
|
|
140
|
+
const normalized = env.trim().toLowerCase();
|
|
141
|
+
return normalized !== "0" && normalized !== "false";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getTriggerLimit(runtime?: IAgentRuntime): number {
|
|
145
|
+
const runtimeSetting = runtime?.getSetting("ELIZA_TRIGGERS_MAX_ACTIVE");
|
|
146
|
+
if (typeof runtimeSetting === "number" && Number.isFinite(runtimeSetting)) {
|
|
147
|
+
return Math.max(1, Math.floor(runtimeSetting));
|
|
148
|
+
}
|
|
149
|
+
if (typeof runtimeSetting === "string" && /^\d+$/.test(runtimeSetting)) {
|
|
150
|
+
return Math.max(1, Number(runtimeSetting));
|
|
151
|
+
}
|
|
152
|
+
const env = process.env.ELIZA_TRIGGERS_MAX_ACTIVE;
|
|
153
|
+
if (env && /^\d+$/.test(env)) {
|
|
154
|
+
return Math.max(1, Number(env));
|
|
155
|
+
}
|
|
156
|
+
return DEFAULT_MAX_ACTIVE_TRIGGERS;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type AutonomyServiceLike = Service & {
|
|
160
|
+
getAutonomousRoomId?: () => UUID;
|
|
161
|
+
getTargetRoomId?: () => UUID;
|
|
162
|
+
/**
|
|
163
|
+
* Toggles the autonomy loop on. Triggers DEPEND on this loop to
|
|
164
|
+
* process the instruction memories they dispatch — without it the
|
|
165
|
+
* memory sits in the room with nothing to act on it. We invoke this
|
|
166
|
+
* on every dispatch (idempotent — no-op when already running) so a
|
|
167
|
+
* cold-start agent doesn't need any extra config.
|
|
168
|
+
*/
|
|
169
|
+
enableAutonomy?: () => Promise<void>;
|
|
170
|
+
isAutonomyEnabled?: () => boolean;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
async function isAutonomyServiceAvailable(
|
|
174
|
+
runtime: IAgentRuntime,
|
|
175
|
+
): Promise<boolean> {
|
|
176
|
+
const svc =
|
|
177
|
+
runtime.getService<AutonomyServiceLike>("AUTONOMY") ??
|
|
178
|
+
runtime.getService<AutonomyServiceLike>("autonomy");
|
|
179
|
+
return svc != null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Dispatch a trigger instruction by creating a memory in the autonomy
|
|
184
|
+
* room. The AutonomyService's internal loop picks up new memories and
|
|
185
|
+
* processes them as autonomous actions.
|
|
186
|
+
*
|
|
187
|
+
* This replaces the previous approach of calling a non-existent
|
|
188
|
+
* `injectAutonomousInstruction` method on the service.
|
|
189
|
+
*/
|
|
190
|
+
async function dispatchInstruction(
|
|
191
|
+
runtime: IAgentRuntime,
|
|
192
|
+
taskId: UUID,
|
|
193
|
+
trigger: TriggerConfig,
|
|
194
|
+
event?: TriggerExecutionOptions["event"],
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
// Resolve the autonomy service to find the target room.
|
|
197
|
+
// Retry up to 5 times (500ms, 1s, 1.5s, 2s backoff) because the
|
|
198
|
+
// service may still be registering after a runtime restart or SQL
|
|
199
|
+
// compatibility repair. Worst case: adds ~5s latency to a trigger
|
|
200
|
+
// dispatch that would have failed anyway. The retry is bounded and
|
|
201
|
+
// does not block the event loop (uses setTimeout).
|
|
202
|
+
let autonomyService: AutonomyServiceLike | null = null;
|
|
203
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
204
|
+
autonomyService =
|
|
205
|
+
runtime.getService<AutonomyServiceLike>("AUTONOMY") ??
|
|
206
|
+
runtime.getService<AutonomyServiceLike>("autonomy");
|
|
207
|
+
if (autonomyService) break;
|
|
208
|
+
if (attempt < 4) {
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!autonomyService) {
|
|
214
|
+
runtime.logger.warn?.(
|
|
215
|
+
`Autonomy service not found after retries (taskId=${taskId}, triggerId=${trigger.triggerId})`,
|
|
216
|
+
);
|
|
217
|
+
throw new Error("Autonomy service unavailable for trigger dispatch");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Auto-enable the autonomy loop if it isn't running. Triggers dispatch
|
|
221
|
+
// instruction memories into the autonomy room and rely on the loop to
|
|
222
|
+
// process them; without this nudge a cold-start agent silently swallows
|
|
223
|
+
// every trigger (memory sits unread in the room until the user manually
|
|
224
|
+
// toggles autonomy). enableAutonomy() is idempotent on the service.
|
|
225
|
+
if (typeof autonomyService.enableAutonomy === "function") {
|
|
226
|
+
try {
|
|
227
|
+
await autonomyService.enableAutonomy();
|
|
228
|
+
runtime.logger.debug?.(
|
|
229
|
+
{
|
|
230
|
+
src: "trigger-runtime",
|
|
231
|
+
agentId: runtime.agentId,
|
|
232
|
+
triggerId: trigger.triggerId,
|
|
233
|
+
},
|
|
234
|
+
"Ensured autonomy loop is enabled for trigger dispatch",
|
|
235
|
+
);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
runtime.logger.warn?.(
|
|
238
|
+
{
|
|
239
|
+
src: "trigger-runtime",
|
|
240
|
+
agentId: runtime.agentId,
|
|
241
|
+
triggerId: trigger.triggerId,
|
|
242
|
+
error: err instanceof Error ? err.message : String(err),
|
|
243
|
+
},
|
|
244
|
+
"Failed to auto-enable autonomy loop — trigger may not be processed",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Resolve the room to inject the instruction into
|
|
250
|
+
const roomId =
|
|
251
|
+
(typeof autonomyService.getAutonomousRoomId === "function"
|
|
252
|
+
? autonomyService.getAutonomousRoomId()
|
|
253
|
+
: undefined) ??
|
|
254
|
+
(typeof autonomyService.getTargetRoomId === "function"
|
|
255
|
+
? autonomyService.getTargetRoomId()
|
|
256
|
+
: undefined);
|
|
257
|
+
|
|
258
|
+
if (!roomId) {
|
|
259
|
+
runtime.logger.warn?.(
|
|
260
|
+
`[trigger-runtime] No autonomy room resolvable for trigger ${trigger.triggerId} — cannot dispatch`,
|
|
261
|
+
);
|
|
262
|
+
throw new Error(
|
|
263
|
+
"No autonomy room available for trigger dispatch. Ensure the AutonomyService has a target room configured.",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Build the trigger instruction memory. Critical: entityId is the
|
|
268
|
+
// dedicated TRIGGER_ENTITY_ID (not runtime.agentId) — otherwise the
|
|
269
|
+
// message-handler pipeline filters it out as "from self" and the
|
|
270
|
+
// agent never runs.
|
|
271
|
+
const eventText = event
|
|
272
|
+
? `\n\nEvent: ${event.kind}\nPayload: ${JSON.stringify(event.payload ?? {})}`
|
|
273
|
+
: "";
|
|
274
|
+
const instructionText = `[Heartbeat: ${trigger.displayName}]\n${trigger.instructions}${eventText}`;
|
|
275
|
+
const instructionMemoryId = stringToUuid(crypto.randomUUID()) as UUID;
|
|
276
|
+
const instructionMemory: Memory = {
|
|
277
|
+
id: instructionMemoryId,
|
|
278
|
+
entityId: TRIGGER_ENTITY_ID,
|
|
279
|
+
agentId: runtime.agentId,
|
|
280
|
+
roomId,
|
|
281
|
+
createdAt: Date.now(),
|
|
282
|
+
content: {
|
|
283
|
+
text: instructionText,
|
|
284
|
+
source: "trigger-runtime",
|
|
285
|
+
metadata: {
|
|
286
|
+
triggerId: trigger.triggerId,
|
|
287
|
+
triggerTaskId: taskId,
|
|
288
|
+
wakeMode: trigger.wakeMode,
|
|
289
|
+
isAutonomousInstruction: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Ensure the trigger entity exists BEFORE inserting the memory. The
|
|
295
|
+
// memories table has a foreign-key constraint on entity_id; inserting
|
|
296
|
+
// a memory authored by a non-existent entity fails with an FK error
|
|
297
|
+
// before we ever reach the pipeline.
|
|
298
|
+
type RuntimeWithEntities = IAgentRuntime & {
|
|
299
|
+
getEntityById?: (id: UUID) => Promise<unknown>;
|
|
300
|
+
createEntity?: (entity: {
|
|
301
|
+
id: UUID;
|
|
302
|
+
names: string[];
|
|
303
|
+
agentId: UUID;
|
|
304
|
+
metadata?: Record<string, unknown>;
|
|
305
|
+
}) => Promise<unknown>;
|
|
306
|
+
upsertEntities?: (
|
|
307
|
+
entities: Array<{
|
|
308
|
+
id: UUID;
|
|
309
|
+
names: string[];
|
|
310
|
+
agentId: UUID;
|
|
311
|
+
metadata?: Record<string, unknown>;
|
|
312
|
+
}>,
|
|
313
|
+
) => Promise<void>;
|
|
314
|
+
};
|
|
315
|
+
const re = runtime as RuntimeWithEntities;
|
|
316
|
+
try {
|
|
317
|
+
const existing = re.getEntityById
|
|
318
|
+
? await re.getEntityById(TRIGGER_ENTITY_ID)
|
|
319
|
+
: null;
|
|
320
|
+
if (!existing) {
|
|
321
|
+
const entity = {
|
|
322
|
+
id: TRIGGER_ENTITY_ID,
|
|
323
|
+
names: ["Trigger"],
|
|
324
|
+
agentId: runtime.agentId,
|
|
325
|
+
metadata: {
|
|
326
|
+
type: "trigger-system",
|
|
327
|
+
description: "Dedicated entity for cron-trigger instructions",
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
if (typeof re.createEntity === "function") {
|
|
331
|
+
try {
|
|
332
|
+
await re.createEntity(entity);
|
|
333
|
+
} catch {
|
|
334
|
+
// Fall back to upsertEntities for adapters that don't expose
|
|
335
|
+
// createEntity (mirrors AutonomyService.ensureAutonomyEntity).
|
|
336
|
+
await re.upsertEntities?.([entity]);
|
|
337
|
+
}
|
|
338
|
+
} else if (typeof re.upsertEntities === "function") {
|
|
339
|
+
await re.upsertEntities([entity]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
runtime.logger.warn?.(
|
|
344
|
+
{
|
|
345
|
+
src: "trigger-runtime",
|
|
346
|
+
error: err instanceof Error ? err.message : String(err),
|
|
347
|
+
},
|
|
348
|
+
"ensureTriggerEntity failed — trigger memory insert will likely fail",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Persist the instruction so it's visible in the autonomy room and
|
|
353
|
+
// returned by the "memories in window" diagnostic on the trigger
|
|
354
|
+
// detail UI.
|
|
355
|
+
await runtime.createMemory(instructionMemory, "messages");
|
|
356
|
+
|
|
357
|
+
// Define a callback that persists EVERY content the agent pipeline
|
|
358
|
+
// emits during this trigger run. Two distinct kinds of content arrive:
|
|
359
|
+
//
|
|
360
|
+
// 1. Action results — when the planner calls e.g. WEB_SEARCH, the
|
|
361
|
+
// action's handler invokes callback(content) with content.action
|
|
362
|
+
// set to the action name and content.text holding the action's
|
|
363
|
+
// output text (Tavily results, FETCH_URL body, etc.).
|
|
364
|
+
//
|
|
365
|
+
// 2. Final response — the LLM's natural-language reply to the user.
|
|
366
|
+
// content.action is typically the planner's chosen primary action
|
|
367
|
+
// or null; text is the human-facing message.
|
|
368
|
+
//
|
|
369
|
+
// We persist both with a `runStage` discriminator so the UI can
|
|
370
|
+
// surface the action output separately from the LLM summary — that
|
|
371
|
+
// way the user sees the raw Tavily results even if the LLM paraphrases
|
|
372
|
+
// them poorly. Server-side logs also breadcrumb each callback so we
|
|
373
|
+
// can trace exactly what the pipeline delivered.
|
|
374
|
+
let callbackCount = 0;
|
|
375
|
+
const callback: HandlerCallback = async (
|
|
376
|
+
content: Content,
|
|
377
|
+
): Promise<Memory[]> => {
|
|
378
|
+
callbackCount += 1;
|
|
379
|
+
const actionName =
|
|
380
|
+
typeof content.action === "string" ? content.action : null;
|
|
381
|
+
const source = typeof content.source === "string" ? content.source : null;
|
|
382
|
+
const textLen =
|
|
383
|
+
typeof content.text === "string" ? content.text.trim().length : 0;
|
|
384
|
+
runtime.logger.info?.(
|
|
385
|
+
{
|
|
386
|
+
src: "trigger-runtime",
|
|
387
|
+
triggerId: trigger.triggerId,
|
|
388
|
+
callbackCount,
|
|
389
|
+
actionName,
|
|
390
|
+
source,
|
|
391
|
+
textLen,
|
|
392
|
+
textPreview:
|
|
393
|
+
typeof content.text === "string" ? content.text.slice(0, 200) : null,
|
|
394
|
+
},
|
|
395
|
+
`Trigger callback #${callbackCount} (action=${actionName ?? "none"} source=${source ?? "none"} textLen=${textLen})`,
|
|
396
|
+
);
|
|
397
|
+
if (textLen === 0) return [];
|
|
398
|
+
|
|
399
|
+
const runStage = actionName && actionName !== "NONE"
|
|
400
|
+
? "action-result"
|
|
401
|
+
: "final-response";
|
|
402
|
+
const responseMemory: Memory = {
|
|
403
|
+
id: stringToUuid(crypto.randomUUID()) as UUID,
|
|
404
|
+
entityId: runtime.agentId,
|
|
405
|
+
agentId: runtime.agentId,
|
|
406
|
+
roomId,
|
|
407
|
+
createdAt: Date.now(),
|
|
408
|
+
content: {
|
|
409
|
+
text: content.text as string,
|
|
410
|
+
source: "trigger-runtime",
|
|
411
|
+
metadata: {
|
|
412
|
+
type: "trigger-response",
|
|
413
|
+
runStage,
|
|
414
|
+
actionName: actionName ?? undefined,
|
|
415
|
+
actionSource: source ?? undefined,
|
|
416
|
+
triggerId: trigger.triggerId,
|
|
417
|
+
triggerTaskId: taskId,
|
|
418
|
+
inReplyTo: instructionMemoryId,
|
|
419
|
+
callbackIndex: callbackCount,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
try {
|
|
424
|
+
await runtime.createMemory(responseMemory, "memories");
|
|
425
|
+
} catch (err) {
|
|
426
|
+
runtime.logger.warn?.(
|
|
427
|
+
{
|
|
428
|
+
src: "trigger-runtime",
|
|
429
|
+
error: err instanceof Error ? err.message : String(err),
|
|
430
|
+
},
|
|
431
|
+
"Failed to persist trigger response memory",
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return [];
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Run the full agent pipeline (providers gather context → planner →
|
|
438
|
+
// actions → response → evaluators) for this instruction. The autonomy
|
|
439
|
+
// service uses the same path; we mirror it so triggers don't depend on
|
|
440
|
+
// the autonomy loop being enabled — that dependency silently swallowed
|
|
441
|
+
// every trigger when autonomy was off (the default).
|
|
442
|
+
//
|
|
443
|
+
// Primary path: runtime.messageService.handleMessage(...). Fallback:
|
|
444
|
+
// emit MESSAGE_RECEIVED for older cores that don't expose the service.
|
|
445
|
+
type MessageServiceLike = {
|
|
446
|
+
handleMessage: (
|
|
447
|
+
runtime: IAgentRuntime,
|
|
448
|
+
message: Memory,
|
|
449
|
+
callback?: HandlerCallback,
|
|
450
|
+
) => Promise<{ didRespond?: boolean; mode?: string } & Record<string, unknown>>;
|
|
451
|
+
};
|
|
452
|
+
const runtimeWithMessageService = runtime as IAgentRuntime & {
|
|
453
|
+
messageService?: MessageServiceLike;
|
|
454
|
+
};
|
|
455
|
+
try {
|
|
456
|
+
if (runtimeWithMessageService.messageService) {
|
|
457
|
+
const result = await runtimeWithMessageService.messageService.handleMessage(
|
|
458
|
+
runtime,
|
|
459
|
+
instructionMemory,
|
|
460
|
+
callback,
|
|
461
|
+
);
|
|
462
|
+
runtime.logger.info?.(
|
|
463
|
+
{
|
|
464
|
+
src: "trigger-runtime",
|
|
465
|
+
agentId: runtime.agentId,
|
|
466
|
+
triggerId: trigger.triggerId,
|
|
467
|
+
didRespond: result?.didRespond,
|
|
468
|
+
},
|
|
469
|
+
`Trigger pipeline complete (didRespond=${result?.didRespond ?? "?"})`,
|
|
470
|
+
);
|
|
471
|
+
} else {
|
|
472
|
+
runtime.logger.warn?.(
|
|
473
|
+
{
|
|
474
|
+
src: "trigger-runtime",
|
|
475
|
+
agentId: runtime.agentId,
|
|
476
|
+
triggerId: trigger.triggerId,
|
|
477
|
+
},
|
|
478
|
+
"messageService unavailable — falling back to MESSAGE_RECEIVED event",
|
|
479
|
+
);
|
|
480
|
+
await runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
|
|
481
|
+
runtime,
|
|
482
|
+
message: instructionMemory,
|
|
483
|
+
callback,
|
|
484
|
+
source: "trigger-runtime",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
runtime.logger.error?.(
|
|
489
|
+
{
|
|
490
|
+
src: "trigger-runtime",
|
|
491
|
+
agentId: runtime.agentId,
|
|
492
|
+
triggerId: trigger.triggerId,
|
|
493
|
+
error: err instanceof Error ? err.message : String(err),
|
|
494
|
+
},
|
|
495
|
+
"Trigger pipeline dispatch failed",
|
|
496
|
+
);
|
|
497
|
+
throw err;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
interface N8nDispatchServiceLike {
|
|
502
|
+
execute(
|
|
503
|
+
workflowId: string,
|
|
504
|
+
payload?: Record<string, unknown>,
|
|
505
|
+
): Promise<{ ok: boolean; error?: string; executionId?: string }>;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function dispatchWorkflow(
|
|
509
|
+
runtime: IAgentRuntime,
|
|
510
|
+
trigger: TriggerConfig,
|
|
511
|
+
event?: TriggerExecutionOptions["event"],
|
|
512
|
+
): Promise<{ ok: true; executionId?: string } | { ok: false; error: string }> {
|
|
513
|
+
if (!trigger.workflowId) {
|
|
514
|
+
return { ok: false, error: "workflow trigger missing workflowId" };
|
|
515
|
+
}
|
|
516
|
+
const svc = runtime.getService<Service & N8nDispatchServiceLike>(
|
|
517
|
+
"N8N_DISPATCH",
|
|
518
|
+
) as (Service & N8nDispatchServiceLike) | null;
|
|
519
|
+
if (!svc) {
|
|
520
|
+
runtime.logger.warn?.(
|
|
521
|
+
{
|
|
522
|
+
src: "trigger-runtime",
|
|
523
|
+
triggerId: trigger.triggerId,
|
|
524
|
+
workflowId: trigger.workflowId,
|
|
525
|
+
},
|
|
526
|
+
"[triggers] workflow dispatch requested but N8N_DISPATCH service not registered",
|
|
527
|
+
);
|
|
528
|
+
return { ok: false, error: "N8N_DISPATCH service not registered" };
|
|
529
|
+
}
|
|
530
|
+
const result = event
|
|
531
|
+
? await svc.execute(trigger.workflowId, {
|
|
532
|
+
eventKind: event.kind,
|
|
533
|
+
eventPayload: event.payload ?? {},
|
|
534
|
+
})
|
|
535
|
+
: await svc.execute(trigger.workflowId);
|
|
536
|
+
return result.ok
|
|
537
|
+
? { ok: true, executionId: result.executionId }
|
|
538
|
+
: { ok: false, error: result.error ?? "workflow execution failed" };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function executeTriggerTask(
|
|
542
|
+
runtime: IAgentRuntime,
|
|
543
|
+
task: Task,
|
|
544
|
+
options: TriggerExecutionOptions,
|
|
545
|
+
): Promise<TriggerExecutionResult> {
|
|
546
|
+
if (!task.id) {
|
|
547
|
+
return { status: "skipped", taskDeleted: false };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const trigger = readTriggerConfig(task);
|
|
551
|
+
if (!trigger) {
|
|
552
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
553
|
+
return { status: "skipped", taskDeleted: false };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!trigger.enabled && !options.force) {
|
|
557
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
558
|
+
return { status: "skipped", taskDeleted: false };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (
|
|
562
|
+
options.source === "event" &&
|
|
563
|
+
trigger.triggerType !== "event" &&
|
|
564
|
+
!options.force
|
|
565
|
+
) {
|
|
566
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
567
|
+
return { status: "skipped", taskDeleted: false };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (
|
|
571
|
+
options.source === "event" &&
|
|
572
|
+
trigger.triggerType === "event" &&
|
|
573
|
+
trigger.eventKind !== options.event?.kind &&
|
|
574
|
+
!options.force
|
|
575
|
+
) {
|
|
576
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
577
|
+
return { status: "skipped", taskDeleted: false };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (
|
|
581
|
+
typeof trigger.maxRuns === "number" &&
|
|
582
|
+
trigger.maxRuns > 0 &&
|
|
583
|
+
trigger.runCount >= trigger.maxRuns
|
|
584
|
+
) {
|
|
585
|
+
await runtime.deleteTask(task.id);
|
|
586
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
587
|
+
return {
|
|
588
|
+
status: "skipped",
|
|
589
|
+
taskDeleted: true,
|
|
590
|
+
trigger: taskToTriggerSummary(task),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const isWorkflowKind = trigger.kind === "workflow";
|
|
595
|
+
|
|
596
|
+
// Workflow-kind triggers dispatch to an external service; they don't
|
|
597
|
+
// require the autonomy room to be ready.
|
|
598
|
+
if (
|
|
599
|
+
!isWorkflowKind &&
|
|
600
|
+
!(await isAutonomyServiceAvailable(runtime)) &&
|
|
601
|
+
options.source !== "manual"
|
|
602
|
+
) {
|
|
603
|
+
runtime.logger.warn?.(
|
|
604
|
+
{
|
|
605
|
+
src: "trigger-runtime",
|
|
606
|
+
taskId: task.id,
|
|
607
|
+
triggerId: trigger.triggerId,
|
|
608
|
+
},
|
|
609
|
+
"Autonomy service unavailable — skipping trigger (will retry next cycle)",
|
|
610
|
+
);
|
|
611
|
+
recordExecutionMetric(runtime.agentId, "skipped", Date.now());
|
|
612
|
+
return { status: "skipped", taskDeleted: false };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const startedAt = Date.now();
|
|
616
|
+
let status: TriggerExecutionResult["status"] = "success";
|
|
617
|
+
let errorMessage = "";
|
|
618
|
+
let workflowExecutionId: string | undefined;
|
|
619
|
+
|
|
620
|
+
if (isWorkflowKind) {
|
|
621
|
+
const result = await dispatchWorkflow(runtime, trigger, options.event);
|
|
622
|
+
if (result.ok === true) {
|
|
623
|
+
workflowExecutionId = result.executionId;
|
|
624
|
+
} else {
|
|
625
|
+
status = "error";
|
|
626
|
+
errorMessage = result.error;
|
|
627
|
+
runtime.logger.error(
|
|
628
|
+
{
|
|
629
|
+
src: "trigger-runtime",
|
|
630
|
+
agentId: runtime.agentId,
|
|
631
|
+
taskId: task.id,
|
|
632
|
+
triggerId: trigger.triggerId,
|
|
633
|
+
workflowId: trigger.workflowId,
|
|
634
|
+
error: errorMessage,
|
|
635
|
+
},
|
|
636
|
+
"Workflow trigger dispatch failed",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
try {
|
|
641
|
+
await dispatchInstruction(runtime, task.id, trigger, options.event);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
status = "error";
|
|
644
|
+
errorMessage = String(error);
|
|
645
|
+
runtime.logger.error(
|
|
646
|
+
{
|
|
647
|
+
src: "trigger-runtime",
|
|
648
|
+
agentId: runtime.agentId,
|
|
649
|
+
taskId: task.id,
|
|
650
|
+
triggerId: trigger.triggerId,
|
|
651
|
+
error: errorMessage,
|
|
652
|
+
},
|
|
653
|
+
"Trigger execution failed",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (status === "success") {
|
|
659
|
+
runtime.logger.info(
|
|
660
|
+
{
|
|
661
|
+
src: "trigger-runtime",
|
|
662
|
+
triggerId: trigger.triggerId,
|
|
663
|
+
triggerName: trigger.displayName,
|
|
664
|
+
triggerType: trigger.triggerType,
|
|
665
|
+
source: options.source,
|
|
666
|
+
latencyMs: Date.now() - startedAt,
|
|
667
|
+
},
|
|
668
|
+
`Trigger "${trigger.displayName}" executed successfully`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const finishedAt = Date.now();
|
|
673
|
+
const runRecord: TriggerRunRecord = {
|
|
674
|
+
triggerRunId: stringToUuid(crypto.randomUUID()),
|
|
675
|
+
triggerId: trigger.triggerId,
|
|
676
|
+
taskId: task.id,
|
|
677
|
+
startedAt,
|
|
678
|
+
finishedAt,
|
|
679
|
+
status,
|
|
680
|
+
error: errorMessage || undefined,
|
|
681
|
+
latencyMs: finishedAt - startedAt,
|
|
682
|
+
source: options.source,
|
|
683
|
+
eventKind: options.event?.kind,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const updatedTrigger: TriggerConfig = {
|
|
687
|
+
...trigger,
|
|
688
|
+
runCount: trigger.runCount + 1,
|
|
689
|
+
lastRunAtIso: new Date(finishedAt).toISOString(),
|
|
690
|
+
lastStatus: status,
|
|
691
|
+
lastError: errorMessage || undefined,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const shouldDeleteTask =
|
|
695
|
+
updatedTrigger.triggerType === "once" ||
|
|
696
|
+
(typeof updatedTrigger.maxRuns === "number" &&
|
|
697
|
+
updatedTrigger.maxRuns > 0 &&
|
|
698
|
+
updatedTrigger.runCount >= updatedTrigger.maxRuns);
|
|
699
|
+
|
|
700
|
+
const existingMetadata = taskMetadata(task);
|
|
701
|
+
const nextMetadata = buildTriggerMetadata({
|
|
702
|
+
existingMetadata,
|
|
703
|
+
trigger: updatedTrigger,
|
|
704
|
+
nowMs: finishedAt,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
let metadataToPersist: TriggerTaskMetadata;
|
|
708
|
+
if (!nextMetadata) {
|
|
709
|
+
metadataToPersist = {
|
|
710
|
+
...existingMetadata,
|
|
711
|
+
updatedAt: finishedAt,
|
|
712
|
+
updateInterval: DISABLED_TRIGGER_INTERVAL_MS,
|
|
713
|
+
trigger: {
|
|
714
|
+
...updatedTrigger,
|
|
715
|
+
enabled: false,
|
|
716
|
+
nextRunAtMs: finishedAt + DISABLED_TRIGGER_INTERVAL_MS,
|
|
717
|
+
lastError:
|
|
718
|
+
updatedTrigger.lastError ?? "Failed to compute next trigger schedule",
|
|
719
|
+
},
|
|
720
|
+
triggerRuns: appendRunRecord(existingMetadata.triggerRuns, runRecord),
|
|
721
|
+
};
|
|
722
|
+
} else {
|
|
723
|
+
metadataToPersist = {
|
|
724
|
+
...nextMetadata,
|
|
725
|
+
triggerRuns: appendRunRecord(existingMetadata.triggerRuns, runRecord),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
await runtime.updateTask(task.id, {
|
|
730
|
+
description: metadataToPersist.trigger?.displayName ?? task.description,
|
|
731
|
+
metadata: metadataToPersist,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const updatedTask: Task = {
|
|
735
|
+
...task,
|
|
736
|
+
description: metadataToPersist.trigger?.displayName ?? task.description,
|
|
737
|
+
metadata: metadataToPersist,
|
|
738
|
+
};
|
|
739
|
+
const triggerSummary = taskToTriggerSummary(updatedTask);
|
|
740
|
+
|
|
741
|
+
if (shouldDeleteTask) {
|
|
742
|
+
await runtime.deleteTask(task.id);
|
|
743
|
+
recordExecutionMetric(runtime.agentId, status, finishedAt);
|
|
744
|
+
return {
|
|
745
|
+
status,
|
|
746
|
+
error: errorMessage || undefined,
|
|
747
|
+
runRecord,
|
|
748
|
+
taskDeleted: true,
|
|
749
|
+
trigger: triggerSummary,
|
|
750
|
+
executionId: workflowExecutionId,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
recordExecutionMetric(runtime.agentId, status, finishedAt);
|
|
755
|
+
return {
|
|
756
|
+
status,
|
|
757
|
+
error: errorMessage || undefined,
|
|
758
|
+
runRecord,
|
|
759
|
+
taskDeleted: false,
|
|
760
|
+
trigger: triggerSummary,
|
|
761
|
+
executionId: workflowExecutionId,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function registerTriggerTaskWorker(runtime: IAgentRuntime): void {
|
|
766
|
+
if (runtime.getTaskWorker(TRIGGER_TASK_NAME)) return;
|
|
767
|
+
|
|
768
|
+
runtime.registerTaskWorker({
|
|
769
|
+
name: TRIGGER_TASK_NAME,
|
|
770
|
+
shouldRun: async () => true,
|
|
771
|
+
execute: async (rt, options, task) => {
|
|
772
|
+
// Return the full result so callers (tests, dashboards) can inspect
|
|
773
|
+
// trigger-specific fields like taskDeleted and runRecord.
|
|
774
|
+
// TaskWorker.execute is typed as returning only scheduling metadata; trigger
|
|
775
|
+
// workers return TriggerExecutionResult for tests and dashboards.
|
|
776
|
+
return (await executeTriggerTask(rt, task, {
|
|
777
|
+
source: options.source === "manual" ? "manual" : "scheduler",
|
|
778
|
+
force: options.force === true,
|
|
779
|
+
})) as unknown as undefined | { nextInterval?: number };
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function listTriggerTasks(
|
|
785
|
+
runtime: IAgentRuntime,
|
|
786
|
+
): Promise<Task[]> {
|
|
787
|
+
if (!triggersFeatureEnabled(runtime)) return [];
|
|
788
|
+
const agentIds = [runtime.agentId];
|
|
789
|
+
const [triggerTasks, heartbeatTasks] = await Promise.all([
|
|
790
|
+
runtime.getTasks({
|
|
791
|
+
agentIds,
|
|
792
|
+
tags: ["repeat", "trigger"],
|
|
793
|
+
}),
|
|
794
|
+
runtime.getTasks({
|
|
795
|
+
agentIds,
|
|
796
|
+
tags: ["repeat", "heartbeat"],
|
|
797
|
+
}),
|
|
798
|
+
]);
|
|
799
|
+
|
|
800
|
+
const merged = new Map<string, Task>();
|
|
801
|
+
for (const task of [...triggerTasks, ...heartbeatTasks]) {
|
|
802
|
+
const key =
|
|
803
|
+
task.id ??
|
|
804
|
+
`${task.name ?? ""}:${task.description ?? ""}:${(task.tags ?? []).join(",")}`;
|
|
805
|
+
if (!merged.has(key)) {
|
|
806
|
+
merged.set(key, task);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return [...merged.values()];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function isExplicitHeartbeatTask(task: Task): boolean {
|
|
813
|
+
const tags = task.tags ?? [];
|
|
814
|
+
return HEARTBEAT_TASK_TAGS.every((tag) => tags.includes(tag));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Derive a friendly display name for a plugin-owned repeat task that
|
|
819
|
+
* doesn't carry explicit trigger metadata. Prefers the task's own
|
|
820
|
+
* `name` (e.g. "IMESSAGE_HEARTBEAT") humanized, then falls back to the
|
|
821
|
+
* first non-generic tag ("imessage", "telegram", etc.), then to a
|
|
822
|
+
* generic "System Heartbeat" label.
|
|
823
|
+
*/
|
|
824
|
+
function deriveSystemHeartbeatName(task: Task): string {
|
|
825
|
+
if (task.name && task.name.length > 0) {
|
|
826
|
+
return task.name
|
|
827
|
+
.replace(/_/g, " ")
|
|
828
|
+
.toLowerCase()
|
|
829
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
830
|
+
}
|
|
831
|
+
const tag = (task.tags ?? []).find(
|
|
832
|
+
(t) => t !== "queue" && t !== "repeat" && t !== "trigger",
|
|
833
|
+
);
|
|
834
|
+
if (tag) {
|
|
835
|
+
return `${tag.charAt(0).toUpperCase()}${tag.slice(1)} Heartbeat`;
|
|
836
|
+
}
|
|
837
|
+
return "System Heartbeat";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Synthesize a read-only TriggerSummary for an explicit heartbeat task
|
|
842
|
+
* that Eliza's trigger schema doesn't fully own. This is narrower than
|
|
843
|
+
* "any repeat task": internal queue drains and runtime schedulers should
|
|
844
|
+
* stay out of the Heartbeats UI even though they also use repeat tasks.
|
|
845
|
+
*/
|
|
846
|
+
function synthesizeSystemHeartbeatSummary(task: Task): TriggerSummary | null {
|
|
847
|
+
if (!task.id) return null;
|
|
848
|
+
const metadata = taskMetadata(task);
|
|
849
|
+
const intervalMs =
|
|
850
|
+
typeof metadata.updateInterval === "number"
|
|
851
|
+
? metadata.updateInterval
|
|
852
|
+
: undefined;
|
|
853
|
+
const tags = task.tags ?? [];
|
|
854
|
+
// Identify the owning plugin from the third tag (first two are "queue"
|
|
855
|
+
// and "repeat"). This becomes createdBy so the UI can group by source.
|
|
856
|
+
const createdBy =
|
|
857
|
+
tags.find((t) => t !== "queue" && t !== "repeat" && t !== "trigger") ??
|
|
858
|
+
"system";
|
|
859
|
+
return {
|
|
860
|
+
id: task.id,
|
|
861
|
+
taskId: task.id,
|
|
862
|
+
displayName: deriveSystemHeartbeatName(task),
|
|
863
|
+
instructions: task.description ?? "",
|
|
864
|
+
triggerType: "interval",
|
|
865
|
+
enabled: true,
|
|
866
|
+
wakeMode: "next_autonomy_cycle",
|
|
867
|
+
createdBy,
|
|
868
|
+
intervalMs,
|
|
869
|
+
runCount: 0,
|
|
870
|
+
updatedAt:
|
|
871
|
+
typeof metadata.updatedAt === "number" ? metadata.updatedAt : undefined,
|
|
872
|
+
updateInterval: intervalMs,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function taskToTriggerSummary(task: Task): TriggerSummary | null {
|
|
877
|
+
const trigger = readTriggerConfig(task);
|
|
878
|
+
if (trigger && task.id) {
|
|
879
|
+
const metadata = taskMetadata(task);
|
|
880
|
+
return {
|
|
881
|
+
id: trigger.triggerId,
|
|
882
|
+
taskId: task.id,
|
|
883
|
+
displayName: trigger.displayName,
|
|
884
|
+
instructions: trigger.instructions,
|
|
885
|
+
triggerType: trigger.triggerType,
|
|
886
|
+
enabled: trigger.enabled,
|
|
887
|
+
wakeMode: trigger.wakeMode,
|
|
888
|
+
createdBy: trigger.createdBy,
|
|
889
|
+
timezone: trigger.timezone,
|
|
890
|
+
intervalMs: trigger.intervalMs,
|
|
891
|
+
scheduledAtIso: trigger.scheduledAtIso,
|
|
892
|
+
cronExpression: trigger.cronExpression,
|
|
893
|
+
eventKind: trigger.eventKind,
|
|
894
|
+
maxRuns: trigger.maxRuns,
|
|
895
|
+
runCount: trigger.runCount,
|
|
896
|
+
nextRunAtMs: trigger.nextRunAtMs,
|
|
897
|
+
lastRunAtIso: trigger.lastRunAtIso,
|
|
898
|
+
lastStatus: trigger.lastStatus,
|
|
899
|
+
lastError: trigger.lastError,
|
|
900
|
+
updatedAt: metadata.updatedAt,
|
|
901
|
+
updateInterval: metadata.updateInterval,
|
|
902
|
+
kind: trigger.kind,
|
|
903
|
+
workflowId: trigger.workflowId,
|
|
904
|
+
workflowName: trigger.workflowName,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (isExplicitHeartbeatTask(task)) {
|
|
909
|
+
return synthesizeSystemHeartbeatSummary(task);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export async function getTriggerHealthSnapshot(
|
|
916
|
+
runtime: IAgentRuntime,
|
|
917
|
+
): Promise<TriggerHealthSnapshot> {
|
|
918
|
+
const tasks = await listTriggerTasks(runtime);
|
|
919
|
+
let activeTriggers = 0;
|
|
920
|
+
let disabledTriggers = 0;
|
|
921
|
+
|
|
922
|
+
let durableExecutions = 0;
|
|
923
|
+
let durableFailures = 0;
|
|
924
|
+
let durableLastExecAt: number | undefined;
|
|
925
|
+
|
|
926
|
+
for (const task of tasks) {
|
|
927
|
+
const trigger = readTriggerConfig(task);
|
|
928
|
+
if (!trigger) continue;
|
|
929
|
+
if (trigger.enabled) {
|
|
930
|
+
activeTriggers += 1;
|
|
931
|
+
} else {
|
|
932
|
+
disabledTriggers += 1;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const runs = readTriggerRuns(task);
|
|
936
|
+
for (const run of runs) {
|
|
937
|
+
durableExecutions += 1;
|
|
938
|
+
if (run.status === "error") durableFailures += 1;
|
|
939
|
+
if (!durableLastExecAt || run.finishedAt > durableLastExecAt) {
|
|
940
|
+
durableLastExecAt = run.finishedAt;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const inMemory = getMetrics(runtime.agentId);
|
|
946
|
+
return {
|
|
947
|
+
triggersEnabled: triggersFeatureEnabled(runtime),
|
|
948
|
+
activeTriggers,
|
|
949
|
+
disabledTriggers,
|
|
950
|
+
totalExecutions: Math.max(inMemory.totalExecutions, durableExecutions),
|
|
951
|
+
totalFailures: Math.max(inMemory.totalFailures, durableFailures),
|
|
952
|
+
totalSkipped: inMemory.totalSkipped,
|
|
953
|
+
lastExecutionAt: inMemory.lastExecutionAt ?? durableLastExecAt,
|
|
954
|
+
};
|
|
955
|
+
}
|