loopengine 0.3.3 → 0.3.5
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/dist/core/run-events.d.ts +21 -0
- package/dist/core/run-events.js +20 -0
- package/dist/core/workflows.d.ts +13 -4
- package/dist/core/workflows.js +43 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/web/agents-config-page.js +113 -24
- package/dist/web/console-shell.js +1 -1
- package/dist/web/workflow-admin.d.ts +14 -3
- package/dist/web/workflow-admin.js +58 -14
- package/dist/web/workflow-page.js +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Message } from './run-agent.js';
|
|
2
|
+
export type RunEventSource = 'workflow' | 'schedule' | 'message' | 'slack' | 'telegram';
|
|
3
|
+
export interface RunEvent {
|
|
4
|
+
at: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
source: RunEventSource;
|
|
7
|
+
/** The same for every report of the same job. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** workflow only: how the run started. */
|
|
10
|
+
via?: string;
|
|
11
|
+
/** message only. */
|
|
12
|
+
caller?: 'api' | 'console' | 'unknown';
|
|
13
|
+
usedTool: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type RunObserver = (event: RunEvent) => void;
|
|
16
|
+
/** Sets the one observer for this process; undefined removes it. */
|
|
17
|
+
export declare function setRunObserver(next: RunObserver | undefined): void;
|
|
18
|
+
/** Never throws: a broken observer must not fail the job. */
|
|
19
|
+
export declare function reportRun(event: Omit<RunEvent, 'at'>): void;
|
|
20
|
+
/** Whether a turn's new messages include a tool call by the agent. */
|
|
21
|
+
export declare function usedTool(newMessages: Message[]): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let observer;
|
|
2
|
+
/** Sets the one observer for this process; undefined removes it. */
|
|
3
|
+
export function setRunObserver(next) {
|
|
4
|
+
observer = next;
|
|
5
|
+
}
|
|
6
|
+
/** Never throws: a broken observer must not fail the job. */
|
|
7
|
+
export function reportRun(event) {
|
|
8
|
+
if (!observer)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
observer({ ...event, at: new Date().toISOString() });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
console.error('[loopengine] run observer failed:', err instanceof Error ? err.message : err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Whether a turn's new messages include a tool call by the agent. */
|
|
18
|
+
export function usedTool(newMessages) {
|
|
19
|
+
return newMessages.some((m) => m.role === 'assistant' && Array.isArray(m.content) && m.content.some((b) => b.type === 'tool_use'));
|
|
20
|
+
}
|
package/dist/core/workflows.d.ts
CHANGED
|
@@ -8,17 +8,26 @@ export interface Workflow {
|
|
|
8
8
|
/** A name for people, e.g. "Refund check". */
|
|
9
9
|
title: string;
|
|
10
10
|
description: string;
|
|
11
|
+
/** A workflow saved before instructions: what its runs began with. Empty
|
|
12
|
+
* for one saved since. */
|
|
11
13
|
startMessage: string;
|
|
12
|
-
/**
|
|
14
|
+
/** A label from before, in words ("Every Monday at 9"); "When asked"
|
|
15
|
+
* when there's none. Nothing reads it to decide when to run. */
|
|
13
16
|
trigger: string;
|
|
14
17
|
parameters: Record<string, WorkflowParameter>;
|
|
15
|
-
/**
|
|
18
|
+
/** The skill body, placeholders unfilled. */
|
|
16
19
|
steps: string;
|
|
20
|
+
/** What the agent does, as one text: the body, with an older
|
|
21
|
+
* workflow's start message in front of it. What the editor shows. */
|
|
22
|
+
instructions: string;
|
|
17
23
|
}
|
|
18
|
-
/** The front matter that makes a skill a workflow
|
|
24
|
+
/** The front matter that makes a skill a workflow: `instructions` (the
|
|
25
|
+
* new way) replaces the body and drops any start message and label;
|
|
26
|
+
* `startMessage` (from before) is kept for workflows saved that way. */
|
|
19
27
|
export interface WorkflowFields {
|
|
20
28
|
title?: string;
|
|
21
|
-
|
|
29
|
+
instructions?: string;
|
|
30
|
+
startMessage?: string;
|
|
22
31
|
trigger?: string;
|
|
23
32
|
parameters?: Record<string, WorkflowParameter>;
|
|
24
33
|
}
|
package/dist/core/workflows.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
// An agent's workflows: skills it can be told to run, by hand, from an app
|
|
2
2
|
// over the API, or on a schedule. A workflow is a skill
|
|
3
|
-
// (agents/<name>/skills/<id>/SKILL.md)
|
|
4
|
-
// run
|
|
3
|
+
// (agents/<name>/skills/<id>/SKILL.md) marked as one, with a title and the
|
|
4
|
+
// values a run is given:
|
|
5
5
|
//
|
|
6
6
|
// ---
|
|
7
7
|
// name: refund-check
|
|
8
8
|
// description: Check a refund request against the order and our policy
|
|
9
|
+
// workflow: true
|
|
9
10
|
// title: Refund check
|
|
10
|
-
// start_message: Check the refund request for order $order_number.
|
|
11
|
-
// trigger: When a refund request comes in
|
|
12
11
|
// parameters:
|
|
13
12
|
// order_number: { description: The order to check, required: true }
|
|
14
13
|
// customer_note: { description: What the customer wrote }
|
|
15
14
|
// ---
|
|
15
|
+
// Check the refund request for order $order_number.
|
|
16
16
|
// 1. Look up order $order_number…
|
|
17
17
|
//
|
|
18
|
-
// The body is its
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
18
|
+
// The body is its instructions: one text, edited as one. A run starts with
|
|
19
|
+
// a line naming the workflow and those instructions, with each $parameter
|
|
20
|
+
// filled in, so the model has them from its first turn rather than
|
|
21
|
+
// picking the skill from its index. In chat, the agent loads the same
|
|
22
|
+
// body through the Skill tool. None of the workflow keys reach the model
|
|
23
|
+
// otherwise: the skill index shows only a skill's name and description
|
|
24
|
+
// (core/skillgarden).
|
|
25
|
+
//
|
|
26
|
+
// What starts a run (schedules, webhooks) isn't kept here: it's the
|
|
27
|
+
// workflow's schedules and webhook triggers themselves.
|
|
28
|
+
//
|
|
29
|
+
// Workflows saved before had a separate start_message (a skill with one
|
|
30
|
+
// is still a workflow) and a `trigger` label; both are folded into the
|
|
31
|
+
// instructions the next time the workflow is saved.
|
|
23
32
|
//
|
|
24
33
|
// Agents from before kept their workflows in workflows.yml;
|
|
25
34
|
// migrateWorkflowsFile moves those into their skills.
|
|
@@ -30,7 +39,7 @@ import { parseSkillFile, substituteParameters } from './skillgarden/index.js';
|
|
|
30
39
|
export class WorkflowParamsError extends Error {
|
|
31
40
|
}
|
|
32
41
|
const PARAMETER_NAME = /^[a-z_][a-z0-9_]*$/;
|
|
33
|
-
const WORKFLOW_KEYS = ['title', 'start_message', 'trigger', 'parameters'];
|
|
42
|
+
const WORKFLOW_KEYS = ['workflow', 'title', 'start_message', 'trigger', 'parameters'];
|
|
34
43
|
function skillsDir(agentDir) {
|
|
35
44
|
return join(agentDir, 'skills');
|
|
36
45
|
}
|
|
@@ -72,9 +81,19 @@ function parametersFrom(value) {
|
|
|
72
81
|
}
|
|
73
82
|
return parameters;
|
|
74
83
|
}
|
|
84
|
+
/** An older workflow's start message and body as one text; the body
|
|
85
|
+
* alone when it already begins with the start message. */
|
|
86
|
+
function combineInstructions(startMessage, body) {
|
|
87
|
+
const steps = body.trim();
|
|
88
|
+
if (!startMessage)
|
|
89
|
+
return steps;
|
|
90
|
+
if (steps.startsWith(startMessage))
|
|
91
|
+
return steps;
|
|
92
|
+
return steps ? `${startMessage}\n\n${steps}` : startMessage;
|
|
93
|
+
}
|
|
75
94
|
function workflowFrom(id, frontmatter, body) {
|
|
76
95
|
const startMessage = typeof frontmatter.start_message === 'string' ? frontmatter.start_message.trim() : '';
|
|
77
|
-
if (!startMessage)
|
|
96
|
+
if (frontmatter.workflow !== true && !startMessage)
|
|
78
97
|
return undefined;
|
|
79
98
|
return {
|
|
80
99
|
id,
|
|
@@ -84,6 +103,7 @@ function workflowFrom(id, frontmatter, body) {
|
|
|
84
103
|
trigger: typeof frontmatter.trigger === 'string' && frontmatter.trigger.trim() ? frontmatter.trigger.trim() : 'When asked',
|
|
85
104
|
parameters: parametersFrom(frontmatter.parameters),
|
|
86
105
|
steps: body,
|
|
106
|
+
instructions: combineInstructions(startMessage, body),
|
|
87
107
|
};
|
|
88
108
|
}
|
|
89
109
|
/** The agent's workflows: its skills that have a start message. */
|
|
@@ -121,12 +141,19 @@ export function setWorkflowFields(agentDir, id, fields) {
|
|
|
121
141
|
const frontmatter = { ...skill.frontmatter };
|
|
122
142
|
for (const key of WORKFLOW_KEYS)
|
|
123
143
|
delete frontmatter[key];
|
|
144
|
+
const parameters = fields.parameters && Object.keys(fields.parameters).length ? { parameters: fields.parameters } : {};
|
|
145
|
+
if (fields.instructions !== undefined) {
|
|
146
|
+
writeSkillFile(path, { ...frontmatter, workflow: true, ...(fields.title ? { title: fields.title } : {}), ...parameters }, fields.instructions);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!fields.startMessage)
|
|
150
|
+
throw new Error('A workflow needs its instructions.');
|
|
124
151
|
writeSkillFile(path, {
|
|
125
152
|
...frontmatter,
|
|
126
153
|
...(fields.title ? { title: fields.title } : {}),
|
|
127
154
|
start_message: fields.startMessage,
|
|
128
155
|
...(fields.trigger ? { trigger: fields.trigger } : {}),
|
|
129
|
-
...
|
|
156
|
+
...parameters,
|
|
130
157
|
}, skill.body);
|
|
131
158
|
}
|
|
132
159
|
/** Stops a skill being a workflow. The skill itself stays. */
|
|
@@ -182,13 +209,15 @@ export function workflowRunMessage(workflow, params = {}) {
|
|
|
182
209
|
// Named placeholders only: positional $1-style filling would eat dollar
|
|
183
210
|
// amounts like "$50" in the steps.
|
|
184
211
|
const fill = (text) => substituteParameters(text, params, { declared, used });
|
|
185
|
-
|
|
212
|
+
// An older workflow begins with its own start message; one saved since
|
|
213
|
+
// with a line naming it, its instructions saying the rest.
|
|
214
|
+
const start = workflow.startMessage ? fill(workflow.startMessage) : `Run the "${workflow.title}" workflow.`;
|
|
186
215
|
const steps = fill(workflow.steps).trim();
|
|
187
216
|
const unused = Object.entries(params).filter(([name]) => !used.has(name));
|
|
188
217
|
return [
|
|
189
218
|
start,
|
|
190
219
|
unused.length ? `Inputs:\n${unused.map(([name, value]) => `- ${name}: ${value}`).join('\n')}` : '',
|
|
191
|
-
steps ?
|
|
220
|
+
steps ? `${workflow.startMessage ? 'These are the steps' : 'These are its instructions'}, from your "${workflow.id}" skill:\n\n${steps}` : '',
|
|
192
221
|
].filter(Boolean).join('\n\n');
|
|
193
222
|
}
|
|
194
223
|
export function legacyWorkflowsPath(agentDir) {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { createAgentEnv, agentDirFor, envScopeOf, type EnvScope } from '#core/ag
|
|
|
7
7
|
export { FileSecretStore, setSecretStore, getSecretStore, type SecretStore } from '#core/secret-store.js';
|
|
8
8
|
export { signProxyIdentity, verifyProxyIdentity, TRUSTED_PROXY_IDENTITY_HEADER, type ProxyIdentity } from '#core/trusted-proxy.js';
|
|
9
9
|
export { setUsageSink, recordUsage, type UsageEvent, type UsageSink } from '#core/usage.js';
|
|
10
|
+
export { setRunObserver, reportRun, usedTool, type RunEvent, type RunEventSource, type RunObserver } from '#core/run-events.js';
|
|
10
11
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS, type NotifierFile, type MessagingApprover } from '#core/notifier-file.js';
|
|
11
12
|
export { listDeliveries, recordDelivery, type NotifierDelivery } from '#core/notifier-deliveries.js';
|
|
12
13
|
export { describeNotifier } from '#core/http-notifier.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { createAgentEnv, agentDirFor, envScopeOf } from '#core/agent-env.js';
|
|
|
9
9
|
export { FileSecretStore, setSecretStore, getSecretStore } from '#core/secret-store.js';
|
|
10
10
|
export { signProxyIdentity, verifyProxyIdentity, TRUSTED_PROXY_IDENTITY_HEADER } from '#core/trusted-proxy.js';
|
|
11
11
|
export { setUsageSink, recordUsage } from '#core/usage.js';
|
|
12
|
+
export { setRunObserver, reportRun, usedTool } from '#core/run-events.js';
|
|
12
13
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS } from '#core/notifier-file.js';
|
|
13
14
|
export { listDeliveries, recordDelivery } from '#core/notifier-deliveries.js';
|
|
14
15
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
@@ -667,8 +667,9 @@ ${consoleShellScript}
|
|
|
667
667
|
(cfg.abilityCount != null
|
|
668
668
|
? card('abilities', 'Apps', String(cfg.abilityCount), cfg.abilityCount ? 'Apps and abilities added to this agent' : 'Add apps and abilities, with the tools you choose')
|
|
669
669
|
: card('abilities', 'Apps', 'Browse', 'Add apps and abilities, with the tools you choose')) +
|
|
670
|
-
card('
|
|
671
|
-
card('
|
|
670
|
+
card('workflows', 'Workflows', '<span id="ovWorkflowsValue">…</span>', '<span id="ovWorkflowsDetail">Loading…</span>') +
|
|
671
|
+
card('tools', 'Tools & skills', cfg.tools.length + (cfg.tools.length === 1 ? ' tool' : ' tools'),
|
|
672
|
+
(cfg.skills.length + (cfg.skills.length === 1 ? ' skill' : ' skills')) + (toolNames.length ? ' \u00b7 ' + escapeHtml(toolNames.slice(0, 2).join(', ') + (toolNames.length > 2 ? ', \u2026' : '')) : '')) +
|
|
672
673
|
card('actauth', 'Permissions', rules.length + (rules.length === 1 ? ' rule' : ' rules'),
|
|
673
674
|
(asks ? asks + ' need approval \u00b7 ' : '') + 'unmatched calls: ' + escapeHtml(cfg.permissions.defaultDecision)) +
|
|
674
675
|
card('messaging', 'Messaging', '<span id="ovMessagingValue">…</span>', '<span id="ovMessagingDetail">Loading…</span>') +
|
|
@@ -683,8 +684,48 @@ ${consoleShellScript}
|
|
|
683
684
|
// cards are filled in once those answer. Called after every render of
|
|
684
685
|
// Overview, which is also re-rendered after edits.
|
|
685
686
|
// ---- Overview: the agent's workflows (web/workflow-admin.ts), added
|
|
686
|
-
// from "Describe a workflow": each with its
|
|
687
|
-
//
|
|
687
|
+
// from "Describe a workflow": each with its instructions, what starts it
|
|
688
|
+
// (its real schedules and webhooks), its recent runs and Run now. ----
|
|
689
|
+
|
|
690
|
+
// What starts a workflow, in a few words, from its schedules and
|
|
691
|
+
// webhooks: never a label that could say something else.
|
|
692
|
+
function workflowStartsSummary(w) {
|
|
693
|
+
var parts = (w.schedules || []).filter(function (x) { return x.repeating && x.status !== 'paused'; }).map(function (x) { return x.describe; });
|
|
694
|
+
if (w.webhooks) parts.push(w.webhooks === 1 ? 'when its webhook is called' : 'when one of its ' + w.webhooks + ' webhooks is called');
|
|
695
|
+
if (!parts.length) return 'When asked';
|
|
696
|
+
var text = parts.join(', and ');
|
|
697
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// How it starts: each schedule (pause, resume, remove), its webhooks,
|
|
701
|
+
// and Run now or the API, which always work.
|
|
702
|
+
function workflowStartsHtml(w) {
|
|
703
|
+
var schedules = (w.schedules || []).map(function (x) {
|
|
704
|
+
var state = x.waiting ? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
705
|
+
: x.status === 'paused' ? '<span class="le-pill">Paused</span>'
|
|
706
|
+
: x.status === 'running' ? '<span class="le-pill accent">Running</span>'
|
|
707
|
+
: (x.nextRunAt ? '<span class="hint">next ' + escapeHtml(new Date(x.nextRunAt).toLocaleString()) + '</span>' : '');
|
|
708
|
+
var values = x.params ? Object.keys(x.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(x.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '';
|
|
709
|
+
return '<li style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
|
|
710
|
+
'<span>' + escapeHtml(x.describe.charAt(0).toUpperCase() + x.describe.slice(1)) + (x.name ? ' <span class="hint">(' + escapeHtml(x.name) + ')</span>' : '') + '</span>' + state +
|
|
711
|
+
(values ? '<span class="hint">' + values + '</span>' : '') +
|
|
712
|
+
(x.notify ? '<span class="hint">sends you each result</span>' : '') +
|
|
713
|
+
'<span style="display:flex;gap:6px;margin-left:auto">' +
|
|
714
|
+
(x.status === 'paused' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="resume">Resume</button>'
|
|
715
|
+
: x.status === 'scheduled' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="pause">Pause</button>' : '') +
|
|
716
|
+
'<button type="button" class="delete-btn workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="cancel">Remove</button>' +
|
|
717
|
+
'</span></li>';
|
|
718
|
+
}).join('');
|
|
719
|
+
return '<div class="body-field workflow-starts" style="margin:6px 0 0">How it starts' +
|
|
720
|
+
'<ul class="schedule-history" style="margin:4px 0 0">' + schedules +
|
|
721
|
+
(w.webhooks ? '<li>' + (w.webhooks === 1 ? 'A webhook' : w.webhooks + ' webhooks') + ' <span class="hint">another app calls it when something happens</span></li>' : '') +
|
|
722
|
+
'<li class="hint">Run now, below, or an app' + String.fromCharCode(8217) + 's API call, any time</li>' +
|
|
723
|
+
'</ul>' +
|
|
724
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">' +
|
|
725
|
+
'<button type="button" class="workflow-add-schedule">Add a schedule</button>' +
|
|
726
|
+
'<button type="button" class="workflow-hooks-btn">' + (w.webhooks ? 'Webhooks' : 'Add a webhook') + '</button>' +
|
|
727
|
+
'</div></div>';
|
|
728
|
+
}
|
|
688
729
|
|
|
689
730
|
function renderWorkflowsHtml(name, workflows) {
|
|
690
731
|
if (!workflows.length) {
|
|
@@ -693,7 +734,7 @@ ${consoleShellScript}
|
|
|
693
734
|
return workflows.map(function (w) {
|
|
694
735
|
var s = w.schedule;
|
|
695
736
|
var state = !s
|
|
696
|
-
? '<span class="hint">' + escapeHtml(w
|
|
737
|
+
? '<span class="hint">' + escapeHtml(workflowStartsSummary(w)) + '</span>'
|
|
697
738
|
: s.waiting
|
|
698
739
|
? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
699
740
|
: s.status === 'paused'
|
|
@@ -707,7 +748,6 @@ ${consoleShellScript}
|
|
|
707
748
|
return '<label class="workflow-param">' + escapeHtml(p) + (spec.required ? '' : ' <span class="hint">(optional)</span>') +
|
|
708
749
|
'<input name="' + escapeHtml(p) + '"' + (spec.required ? ' required' : '') + ' placeholder="' + escapeHtml(spec.description || '') + '"></label>';
|
|
709
750
|
}).join('');
|
|
710
|
-
var scheduleCount = (w.schedules || []).filter(function (x) { return x.repeating; }).length;
|
|
711
751
|
var runs = (w.recentRuns || []).map(function (r) {
|
|
712
752
|
var pill = r.status === 'done' ? '<span class="le-pill good">Done</span>'
|
|
713
753
|
: r.status === 'error' ? '<span class="le-pill bad">Failed</span>'
|
|
@@ -722,22 +762,44 @@ ${consoleShellScript}
|
|
|
722
762
|
return '<div class="source workflow-card" data-workflow="' + escapeHtml(w.id) + '">' +
|
|
723
763
|
'<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:baseline">' +
|
|
724
764
|
'<strong>' + escapeHtml(w.title) + '</strong><span>' + state + '</span></div>' +
|
|
725
|
-
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(w
|
|
726
|
-
'<details><summary class="hint">
|
|
765
|
+
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(workflowStartsSummary(w)) : '') + '</p>' +
|
|
766
|
+
'<details><summary class="hint">Instructions <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.instructions) + '</div></details>' +
|
|
767
|
+
workflowStartsHtml(w) +
|
|
727
768
|
(params ? '<div class="workflow-params">' + params + '</div>' : '') +
|
|
728
769
|
(runs ? '<details class="workflow-runs"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>' : '') +
|
|
729
770
|
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">' +
|
|
730
771
|
'<button type="button" class="workflow-run">Run now</button>' +
|
|
731
|
-
(scheduleCount ? '<a href="?tab=schedules" data-tab="schedules" class="hint" style="align-self:center">' + (scheduleCount > 1 ? 'Schedules (' + scheduleCount + ')' : 'Schedule') + '</a>' : '') +
|
|
732
772
|
'<button type="button" class="workflow-edit">Edit</button>' +
|
|
733
|
-
'<button type="button" class="workflow-hooks-btn">Webhooks</button>' +
|
|
734
|
-
'<a href="?tab=skills" data-tab="skills" class="hint" style="align-self:center">Edit steps</a>' +
|
|
735
773
|
'<button type="button" class="delete-btn workflow-forget">Forget</button>' +
|
|
736
774
|
'</div><div class="hint workflow-result"></div><div class="workflow-editor" hidden></div><div class="workflow-hooks" hidden></div></div>';
|
|
737
775
|
}).join('') + '<p class="hint" style="margin:6px 0 0"><a href="/workflows/new">Describe another workflow</a></p>';
|
|
738
776
|
}
|
|
739
777
|
|
|
778
|
+
// The Overview's Workflows tile: how many, the next scheduled run, and
|
|
779
|
+
// any waiting for a decision.
|
|
780
|
+
function fillWorkflowsCard(workflows) {
|
|
781
|
+
var value = detail.querySelector('#ovWorkflowsValue');
|
|
782
|
+
var detailEl = detail.querySelector('#ovWorkflowsDetail');
|
|
783
|
+
if (!value || !detailEl) return;
|
|
784
|
+
value.textContent = String(workflows.length);
|
|
785
|
+
var next = null;
|
|
786
|
+
var waiting = 0;
|
|
787
|
+
workflows.forEach(function (w) {
|
|
788
|
+
(w.schedules || []).forEach(function (x) {
|
|
789
|
+
if (x.waiting) waiting++;
|
|
790
|
+
if (x.status === 'scheduled' && x.nextRunAt && (!next || x.nextRunAt < next)) next = x.nextRunAt;
|
|
791
|
+
});
|
|
792
|
+
(w.recentRuns || []).forEach(function (r) { if (r.status === 'awaiting_approval') waiting++; });
|
|
793
|
+
});
|
|
794
|
+
var parts = [];
|
|
795
|
+
if (waiting) parts.push(waiting + ' waiting for a decision');
|
|
796
|
+
if (next) parts.push('next run ' + new Date(next).toLocaleString());
|
|
797
|
+
detailEl.textContent = parts.join(' \u00b7 ') || (workflows.length ? 'Run by hand, by an app or a webhook' : 'Describe a job for this agent to do');
|
|
798
|
+
}
|
|
799
|
+
|
|
740
800
|
var workflowsRefreshTimer = null;
|
|
801
|
+
// Set by a workflow's "Add a schedule", for the Schedules tab's form.
|
|
802
|
+
var pendingScheduleWorkflow = null;
|
|
741
803
|
var workflowsById = {};
|
|
742
804
|
// Set while a workflow's editor is open, so the running-run refresh
|
|
743
805
|
// doesn't redraw the card under someone typing.
|
|
@@ -751,6 +813,7 @@ ${consoleShellScript}
|
|
|
751
813
|
if (currentName !== name) return;
|
|
752
814
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
753
815
|
var workflows = r.body.workflows || [];
|
|
816
|
+
fillWorkflowsCard(workflows);
|
|
754
817
|
workflowsById = {};
|
|
755
818
|
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
756
819
|
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
@@ -758,12 +821,12 @@ ${consoleShellScript}
|
|
|
758
821
|
for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
|
|
759
822
|
// Keep a running run's status current while the Overview is open.
|
|
760
823
|
var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
|
|
761
|
-
if (running && currentTab === '
|
|
824
|
+
if (running && currentTab === 'workflows') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
|
|
762
825
|
});
|
|
763
826
|
}
|
|
764
827
|
|
|
765
|
-
// ---- The workflow editor:
|
|
766
|
-
// the
|
|
828
|
+
// ---- The workflow editor: its title, its instructions (the skill body)
|
|
829
|
+
// and the values a run is given. What starts it is on the card.
|
|
767
830
|
function workflowParamRowHtml(pname, spec) {
|
|
768
831
|
return '<div class="workflow-param-row">' +
|
|
769
832
|
'<input class="wp-name" aria-label="Parameter name" placeholder="order_number" value="' + escapeHtml(pname || '') + '">' +
|
|
@@ -776,9 +839,8 @@ ${consoleShellScript}
|
|
|
776
839
|
var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
|
|
777
840
|
return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
|
|
778
841
|
'<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"></label>' +
|
|
779
|
-
'<label>
|
|
780
|
-
'<textarea name="
|
|
781
|
-
'<label>When it runs <span class="hint">(in words, for people; the timing itself is its schedule)</span><input name="trigger" maxlength="200" value="' + escapeHtml(w.trigger) + '"></label>' +
|
|
842
|
+
'<label>Instructions <span class="hint">(what the agent does, step by step; $name puts in a parameter's value. Chat uses them too.)</span>' +
|
|
843
|
+
'<textarea name="instructions" maxlength="12000" rows="12" required>' + escapeHtml(w.instructions) + '</textarea></label>' +
|
|
782
844
|
'<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
|
|
783
845
|
'<div class="workflow-param-rows">' + rows + '</div>' +
|
|
784
846
|
'<button type="button" class="wp-add" style="align-self:flex-start">Add parameter</button></div>' +
|
|
@@ -787,12 +849,12 @@ ${consoleShellScript}
|
|
|
787
849
|
'<div class="hint workflow-editor-result"></div></form>';
|
|
788
850
|
}
|
|
789
851
|
|
|
790
|
-
// Placeholders
|
|
791
|
-
//
|
|
852
|
+
// Placeholders the instructions use that no parameter declares, and
|
|
853
|
+
// declared parameters nothing uses.
|
|
792
854
|
function workflowPlaceholderCheck(w, form) {
|
|
793
855
|
var used = {};
|
|
794
856
|
var pattern = new RegExp('[$]([a-z_][a-z0-9_]*)', 'g');
|
|
795
|
-
var text = form.querySelector('[name="
|
|
857
|
+
var text = form.querySelector('[name="instructions"]').value;
|
|
796
858
|
var m;
|
|
797
859
|
while ((m = pattern.exec(text))) used[m[1]] = true;
|
|
798
860
|
var declared = {};
|
|
@@ -802,7 +864,7 @@ ${consoleShellScript}
|
|
|
802
864
|
var missing = Object.keys(used).filter(function (n) { return !declared[n]; });
|
|
803
865
|
var unused = Object.keys(declared).filter(function (n) { return !used[n]; });
|
|
804
866
|
if (missing.length) notes.push('Uses ' + missing.map(function (n) { return '$' + n; }).join(', ') + ', which ' + (missing.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't a parameter: add ' + (missing.length > 1 ? 'them' : 'it') + ', or the text stays as written.');
|
|
805
|
-
if (unused.length) notes.push(unused.join(', ') + ' ' + (unused.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't used in the
|
|
867
|
+
if (unused.length) notes.push(unused.join(', ') + ' ' + (unused.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't used in the instructions: its value is passed as an input.');
|
|
806
868
|
form.querySelector('.workflow-editor-check').textContent = notes.join(' ');
|
|
807
869
|
}
|
|
808
870
|
|
|
@@ -843,7 +905,7 @@ ${consoleShellScript}
|
|
|
843
905
|
result.textContent = 'Saving' + String.fromCharCode(8230);
|
|
844
906
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
|
|
845
907
|
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
846
|
-
body: JSON.stringify({ title: form.title.value,
|
|
908
|
+
body: JSON.stringify({ title: form.title.value, instructions: form.instructions.value, parameters: parameters }),
|
|
847
909
|
}).then(function (r) {
|
|
848
910
|
if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
|
|
849
911
|
workflowEditing = false;
|
|
@@ -1040,6 +1102,21 @@ ${consoleShellScript}
|
|
|
1040
1102
|
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
|
|
1041
1103
|
card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
|
|
1042
1104
|
card.querySelector('.workflow-hooks-btn').addEventListener('click', function () { openWorkflowHooks(name, card); });
|
|
1105
|
+
// Add a schedule: the Schedules tab's form, with this workflow chosen.
|
|
1106
|
+
card.querySelector('.workflow-add-schedule').addEventListener('click', function () {
|
|
1107
|
+
pendingScheduleWorkflow = id;
|
|
1108
|
+
switchTab('schedules');
|
|
1109
|
+
});
|
|
1110
|
+
var scheduleBtns = card.querySelectorAll('.workflow-schedule-action');
|
|
1111
|
+
for (var sb = 0; sb < scheduleBtns.length; sb++) {
|
|
1112
|
+
scheduleBtns[sb].addEventListener('click', function (ev) {
|
|
1113
|
+
var btn = ev.currentTarget;
|
|
1114
|
+
var action = btn.dataset.action;
|
|
1115
|
+
if (action === 'cancel' && btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
1116
|
+
btn.disabled = true;
|
|
1117
|
+
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(btn.dataset.id) + '/' + action, { method: 'POST' }).then(function () { loadWorkflowsSection(name); });
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1043
1120
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
1044
1121
|
var btn = ev.currentTarget;
|
|
1045
1122
|
var params = {};
|
|
@@ -1153,7 +1230,6 @@ ${consoleShellScript}
|
|
|
1153
1230
|
}
|
|
1154
1231
|
|
|
1155
1232
|
return renderOverviewCards(cfg) +
|
|
1156
|
-
'<section id="workflowsSection"><h3>Workflows</h3><div id="workflowsContent"><p class="hint">Loading…</p></div></section>' +
|
|
1157
1233
|
'<section>' + systemPromptSection + '</section>' +
|
|
1158
1234
|
'<section>' + modelSection + '</section>' +
|
|
1159
1235
|
'<section><h3>Hooks</h3><dl class="kv">' +
|
|
@@ -2353,6 +2429,14 @@ ${consoleShellScript}
|
|
|
2353
2429
|
'<input name="param:' + escapeHtml(p) + '" placeholder="' + escapeHtml(parameters[p].description || '') + '"></label>';
|
|
2354
2430
|
}).join('');
|
|
2355
2431
|
}
|
|
2432
|
+
// Opened from a workflow's "Add a schedule": that workflow, chosen.
|
|
2433
|
+
if (pendingScheduleWorkflow && workflowSelect) {
|
|
2434
|
+
workflowSelect.value = pendingScheduleWorkflow;
|
|
2435
|
+
var runsWorkflowRadio = form.querySelector('#runsWorkflow');
|
|
2436
|
+
if (runsWorkflowRadio) runsWorkflowRadio.checked = true;
|
|
2437
|
+
form.scrollIntoView({ block: 'start' });
|
|
2438
|
+
}
|
|
2439
|
+
pendingScheduleWorkflow = null;
|
|
2356
2440
|
renderScheduleParams();
|
|
2357
2441
|
if (workflowSelect) workflowSelect.addEventListener('change', renderScheduleParams);
|
|
2358
2442
|
for (var k = 0; k < runsRadios.length; k++) {
|
|
@@ -4147,7 +4231,7 @@ ${consoleShellScript}
|
|
|
4147
4231
|
|
|
4148
4232
|
// ---- Tab switching + agent selection ----
|
|
4149
4233
|
|
|
4150
|
-
var CONFIG_TABS = ['overview', 'abilities', 'actauth', 'messaging', 'schedules', 'env', 'advanced'];
|
|
4234
|
+
var CONFIG_TABS = ['overview', 'workflows', 'abilities', 'actauth', 'messaging', 'schedules', 'env', 'advanced'];
|
|
4151
4235
|
// The old Tools and Skills tabs: now parts of Advanced. A link to one
|
|
4152
4236
|
// opens Advanced at that part.
|
|
4153
4237
|
// The Apps tab keeps the id 'abilities'; ?tab=apps opens it too.
|
|
@@ -4284,6 +4368,11 @@ ${consoleShellScript}
|
|
|
4284
4368
|
}) +
|
|
4285
4369
|
'<div class="le-scroll"><div class="le-page" style="display:block">' +
|
|
4286
4370
|
'<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
|
|
4371
|
+
// Workflows: the jobs this agent does, each with what starts it.
|
|
4372
|
+
'<div class="tab-panel" data-tab-panel="workflows">' +
|
|
4373
|
+
'<p class="muted" style="margin:0 0 20px">The jobs this agent does on its own: what it does, what starts it (schedules, webhooks, Run now or an app's API call) and how its runs went. In Chat it can do them too, when you ask.</p>' +
|
|
4374
|
+
'<section id="workflowsSection"><div id="workflowsContent"><p class="hint">Loading…</p></div></section>' +
|
|
4375
|
+
'</div>' +
|
|
4287
4376
|
// Advanced: the agent's own tools (and Composio, subagents), then its
|
|
4288
4377
|
// own skills. Each part keeps its old data-tab-panel name, which the
|
|
4289
4378
|
// code that redraws it looks for; only .tab-panel is shown or hidden.
|
|
@@ -56,7 +56,7 @@ export const consoleShellScript = `<script>
|
|
|
56
56
|
// ability on the Apps tab (id 'abilities', so older links still work).
|
|
57
57
|
// ?tab=tools and ?tab=skills still open Advanced.
|
|
58
58
|
var TABS = [
|
|
59
|
-
['overview', 'Overview'], ['chat', 'Chat'], ['abilities', 'Apps'], ['actauth', 'Permissions'],
|
|
59
|
+
['overview', 'Overview'], ['workflows', 'Workflows'], ['chat', 'Chat'], ['abilities', 'Apps'], ['actauth', 'Permissions'],
|
|
60
60
|
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment'], ['advanced', 'Advanced']
|
|
61
61
|
];
|
|
62
62
|
|
|
@@ -5,6 +5,7 @@ import { type GeneratedMcpAbility, type McpAbilityRequest } from '../core/mcp-ab
|
|
|
5
5
|
import type { McpDirectoryServer } from '../core/mcp-directory.js';
|
|
6
6
|
import { type Workflow } from '../core/workflows.js';
|
|
7
7
|
import { type WorkflowRun } from '../core/workflow-runs.js';
|
|
8
|
+
import { type ScheduledTask } from '../core/scheduler.js';
|
|
8
9
|
import type { ModelCall } from '../core/run-agent.js';
|
|
9
10
|
export declare class WorkflowError extends Error {
|
|
10
11
|
}
|
|
@@ -162,6 +163,9 @@ type WorkflowScheduleView = {
|
|
|
162
163
|
taskId: string;
|
|
163
164
|
status: string;
|
|
164
165
|
repeating: boolean;
|
|
166
|
+
describe: string;
|
|
167
|
+
name?: string;
|
|
168
|
+
notify: boolean;
|
|
165
169
|
params?: Record<string, string>;
|
|
166
170
|
nextRunAt?: string;
|
|
167
171
|
lastRunAt?: string;
|
|
@@ -175,7 +179,12 @@ export interface AgentWorkflowView extends Workflow {
|
|
|
175
179
|
schedules: WorkflowScheduleView[];
|
|
176
180
|
/** Its latest runs started by hand or by an app, newest first. */
|
|
177
181
|
recentRuns: WorkflowRun[];
|
|
182
|
+
/** Its webhook triggers that are on. */
|
|
183
|
+
webhooks: number;
|
|
178
184
|
}
|
|
185
|
+
/** A schedule in words, in its time zone: "every day at 18:05", "every
|
|
186
|
+
* Monday at 09:00", "every 10 minutes", "once on Nov 6 at 18:05". */
|
|
187
|
+
export declare function plainSchedule(schedule: ScheduledTask['schedule']): string;
|
|
179
188
|
/** Moves an agent's workflows.yml, from before workflows were skills, into
|
|
180
189
|
* its skills, and points the schedules it linked by name at the workflows
|
|
181
190
|
* they run. Safe to call any time: it does nothing once the file is gone. */
|
|
@@ -192,9 +201,11 @@ export declare function prepareWorkflowRun(agentName: string, agentDir: string,
|
|
|
192
201
|
params: Record<string, string>;
|
|
193
202
|
message: string;
|
|
194
203
|
}>;
|
|
195
|
-
/** Changes
|
|
196
|
-
*
|
|
197
|
-
*
|
|
204
|
+
/** Changes a workflow from the console's editor: its title, its
|
|
205
|
+
* instructions (the skill body, which an older workflow's start message
|
|
206
|
+
* and label are folded into), and its parameters. The skill's other front
|
|
207
|
+
* matter is kept. A body with `start_message` (and no `instructions`) is
|
|
208
|
+
* still taken, as before. */
|
|
198
209
|
export declare function updateWorkflow(agentDir: string, id: string, input: Record<string, unknown>): Workflow;
|
|
199
210
|
/** Forgets a workflow: its skill stays, as a plain skill, and the
|
|
200
211
|
* schedules that ran it are cancelled. Returns how many were. */
|
|
@@ -11,12 +11,12 @@ import { writeSkill } from './skills-admin.js';
|
|
|
11
11
|
import { createFromTemplateObject, installFailureReason, templateChecklist } from './templates-admin.js';
|
|
12
12
|
import { templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
13
|
import { planParameters, planWebhook, planWorkflowWithModel, slugify, workflowTemplate } from '../core/system-agents/workflow-planner.js';
|
|
14
|
-
import { createTrigger, publicTrigger } from '../core/workflow-triggers.js';
|
|
14
|
+
import { createTrigger, listTriggers, publicTrigger } from '../core/workflow-triggers.js';
|
|
15
15
|
import { mcpAbilityNames } from '../core/mcp-ability-generator.js';
|
|
16
16
|
import { clearWorkflowFields, migrateWorkflowsFile, readWorkflow, readWorkflows, setWorkflowFields, validateParams, workflowRunMessage } from '../core/workflows.js';
|
|
17
17
|
import { isUnderWay, listRuns } from '../core/workflow-runs.js';
|
|
18
|
-
import { cancelScheduledTask, createScheduledTask, getSchedulerStore, listScheduledTasks, pauseScheduledTask } from '../core/scheduler.js';
|
|
19
|
-
import { workspaceTimeZone } from '../core/time-zone.js';
|
|
18
|
+
import { cancelScheduledTask, createScheduledTask, describeSchedule, getSchedulerStore, listScheduledTasks, pauseScheduledTask } from '../core/scheduler.js';
|
|
19
|
+
import { wallTimeAt, workspaceTimeZone } from '../core/time-zone.js';
|
|
20
20
|
export class WorkflowError extends Error {
|
|
21
21
|
}
|
|
22
22
|
/** Checks a plan against the agent it would be added to. */
|
|
@@ -316,7 +316,7 @@ async function makePlannedWebhook(plan, agent, agentDir) {
|
|
|
316
316
|
webhook: { trigger: publicTrigger(trigger), token, ...(webhook.setup ? { setup: webhook.setup } : {}) },
|
|
317
317
|
checklist: [{
|
|
318
318
|
label: `Turn on the ${webhook.name} webhook: paste its URL into ${SENDER_NAMES[webhook.sender]}'s webhook settings, then add the signing secret ${webhook.sender === 'custom' ? 'your app signs with' : 'it shows'}`,
|
|
319
|
-
href: `/agents/config?agent=${encodeURIComponent(agent)}&tab=
|
|
319
|
+
href: `/agents/config?agent=${encodeURIComponent(agent)}&tab=workflows`,
|
|
320
320
|
}],
|
|
321
321
|
};
|
|
322
322
|
}
|
|
@@ -342,7 +342,9 @@ export async function createWorkflow(input, deps) {
|
|
|
342
342
|
// matter. Its parameters are tidied again, as the plan may have been
|
|
343
343
|
// edited on the review screen.
|
|
344
344
|
const { parameters } = planParameters(plan.parameters, { startMessage: plan.startMessage, skillBody: plan.skill.body, scheduled: plan.trigger.kind === 'schedule' });
|
|
345
|
-
|
|
345
|
+
// One text, as the editor shows it: the start message, then the steps.
|
|
346
|
+
const instructions = plan.skill.body.trim().startsWith(plan.startMessage.trim()) ? plan.skill.body : `${plan.startMessage.trim()}\n\n${plan.skill.body.trim()}`;
|
|
347
|
+
const markWorkflow = (agentDir) => setWorkflowFields(agentDir, plan.id, { title: plan.name, instructions, parameters });
|
|
346
348
|
const agents = await deps.agentInfo();
|
|
347
349
|
resolveConnections(plan, agents);
|
|
348
350
|
const keys = input.mcpKeys && typeof input.mcpKeys === 'object' ? Object.fromEntries(Object.entries(input.mcpKeys).flatMap(([k, v]) => (typeof v === 'string' && v.trim() ? [[k, v.trim()]] : []))) : {};
|
|
@@ -491,11 +493,46 @@ function workflowTasks(id, tasks) {
|
|
|
491
493
|
.filter((t) => t.workflow_id === id && t.status !== 'cancelled' && t.status !== 'done')
|
|
492
494
|
.sort((a, b) => Number('every' in b.schedule) - Number('every' in a.schedule));
|
|
493
495
|
}
|
|
496
|
+
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
497
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
498
|
+
/** A schedule in words, in its time zone: "every day at 18:05", "every
|
|
499
|
+
* Monday at 09:00", "every 10 minutes", "once on Nov 6 at 18:05". */
|
|
500
|
+
export function plainSchedule(schedule) {
|
|
501
|
+
const zone = ('time_zone' in schedule && schedule.time_zone) || workspaceTimeZone();
|
|
502
|
+
const at = (iso) => {
|
|
503
|
+
const t = wallTimeAt(Date.parse(iso), zone);
|
|
504
|
+
return { ...t, clock: `${String(t.hour).padStart(2, '0')}:${String(t.minute).padStart(2, '0')}` };
|
|
505
|
+
};
|
|
506
|
+
if ('run_at' in schedule) {
|
|
507
|
+
if (Number.isNaN(Date.parse(schedule.run_at)))
|
|
508
|
+
return `once at ${schedule.run_at}`;
|
|
509
|
+
const t = at(schedule.run_at);
|
|
510
|
+
return `once on ${MONTHS[t.month - 1]} ${t.day} at ${t.clock}`;
|
|
511
|
+
}
|
|
512
|
+
const m = /^(\d+)([mhdw])$/.exec(schedule.every);
|
|
513
|
+
if (!m)
|
|
514
|
+
return describeSchedule(schedule);
|
|
515
|
+
const n = Number(m[1]);
|
|
516
|
+
const unit = m[2];
|
|
517
|
+
const anchor = schedule.anchor && !Number.isNaN(Date.parse(schedule.anchor)) ? at(schedule.anchor) : undefined;
|
|
518
|
+
if (unit === 'm')
|
|
519
|
+
return n === 1 ? 'every minute' : `every ${n} minutes`;
|
|
520
|
+
if (unit === 'h')
|
|
521
|
+
return (n === 1 ? 'every hour' : `every ${n} hours`) + (anchor && n === 1 ? ` at :${String(anchor.minute).padStart(2, '0')}` : '');
|
|
522
|
+
if (unit === 'd')
|
|
523
|
+
return (n === 1 ? 'every day' : `every ${n} days`) + (anchor ? ` at ${anchor.clock}` : '');
|
|
524
|
+
if (n === 1 && anchor)
|
|
525
|
+
return `every ${WEEKDAYS[anchor.weekday]} at ${anchor.clock}`;
|
|
526
|
+
return (n === 1 ? 'every week' : `every ${n} weeks`) + (anchor ? ` on ${WEEKDAYS[anchor.weekday]} at ${anchor.clock}` : '');
|
|
527
|
+
}
|
|
494
528
|
function scheduleView(task) {
|
|
495
529
|
return {
|
|
496
530
|
taskId: task.task_id,
|
|
497
531
|
status: task.status,
|
|
498
532
|
repeating: 'every' in task.schedule,
|
|
533
|
+
describe: plainSchedule(task.schedule),
|
|
534
|
+
...(task.task_name ? { name: task.task_name } : {}),
|
|
535
|
+
notify: Boolean(task.notify),
|
|
499
536
|
...(task.params ? { params: task.params } : {}),
|
|
500
537
|
...(task.next_run_at ? { nextRunAt: task.next_run_at } : {}),
|
|
501
538
|
...(task.last_run_at ? { lastRunAt: task.last_run_at } : {}),
|
|
@@ -525,10 +562,12 @@ export async function listAgentWorkflows(agentName, agentDir) {
|
|
|
525
562
|
return [];
|
|
526
563
|
const tasks = await listScheduledTasks({ agent: agentName });
|
|
527
564
|
const runs = await listRuns(agentName);
|
|
565
|
+
const triggers = await listTriggers(agentName);
|
|
528
566
|
return workflows.map((w) => {
|
|
529
567
|
const schedules = workflowTasks(w.id, tasks).map(scheduleView);
|
|
530
568
|
const recentRuns = runs.filter((r) => r.workflow_id === w.id).slice(0, RECENT_RUNS);
|
|
531
|
-
|
|
569
|
+
const webhooks = triggers.filter((t) => t.workflow_id === w.id && t.enabled).length;
|
|
570
|
+
return { ...w, schedules, recentRuns, webhooks, ...(schedules[0] ? { schedule: schedules[0] } : {}) };
|
|
532
571
|
});
|
|
533
572
|
}
|
|
534
573
|
function sameParams(a, b) {
|
|
@@ -558,9 +597,11 @@ export async function prepareWorkflowRun(agentName, agentDir, id, rawParams) {
|
|
|
558
597
|
}
|
|
559
598
|
return { workflow, params, message: workflowRunMessage(workflow, params) };
|
|
560
599
|
}
|
|
561
|
-
/** Changes
|
|
562
|
-
*
|
|
563
|
-
*
|
|
600
|
+
/** Changes a workflow from the console's editor: its title, its
|
|
601
|
+
* instructions (the skill body, which an older workflow's start message
|
|
602
|
+
* and label are folded into), and its parameters. The skill's other front
|
|
603
|
+
* matter is kept. A body with `start_message` (and no `instructions`) is
|
|
604
|
+
* still taken, as before. */
|
|
564
605
|
export function updateWorkflow(agentDir, id, input) {
|
|
565
606
|
if (!readWorkflow(agentDir, id))
|
|
566
607
|
throw new WorkflowError(`No workflow ${id}.`);
|
|
@@ -573,10 +614,13 @@ export function updateWorkflow(agentDir, id, input) {
|
|
|
573
614
|
return trimmed;
|
|
574
615
|
};
|
|
575
616
|
const title = text(input.title, 'The title', 120);
|
|
576
|
-
const
|
|
577
|
-
if (!
|
|
578
|
-
throw new WorkflowError('Write the
|
|
579
|
-
const
|
|
617
|
+
const instructions = input.instructions === undefined ? undefined : text(input.instructions, 'The instructions', 12000);
|
|
618
|
+
if (instructions !== undefined && !instructions)
|
|
619
|
+
throw new WorkflowError('Write the instructions: what the agent does, e.g. "Check the refund request for order $order_number: look up the order, then…"');
|
|
620
|
+
const startMessage = instructions === undefined ? text(input.start_message, 'The start message', 4000) : '';
|
|
621
|
+
if (instructions === undefined && !startMessage)
|
|
622
|
+
throw new WorkflowError('Write the instructions: what the agent does each run.');
|
|
623
|
+
const trigger = instructions === undefined ? text(input.trigger, 'When it runs', 200) : '';
|
|
580
624
|
const raw = input.parameters ?? {};
|
|
581
625
|
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
582
626
|
throw new WorkflowError('parameters must be an object of names to { description, required }.');
|
|
@@ -587,7 +631,7 @@ export function updateWorkflow(agentDir, id, input) {
|
|
|
587
631
|
const s = spec && typeof spec === 'object' ? spec : {};
|
|
588
632
|
parameters[name] = { description: text(s.description, `The description of "${name}"`, 300), required: s.required === true };
|
|
589
633
|
}
|
|
590
|
-
setWorkflowFields(agentDir, id, { ...(title ? { title } : {}), startMessage, ...(trigger ? { trigger } : {}), parameters });
|
|
634
|
+
setWorkflowFields(agentDir, id, instructions !== undefined ? { ...(title ? { title } : {}), instructions, parameters } : { ...(title ? { title } : {}), startMessage, ...(trigger ? { trigger } : {}), parameters });
|
|
591
635
|
return readWorkflow(agentDir, id);
|
|
592
636
|
}
|
|
593
637
|
/** Forgets a workflow: its skill stays, as a plain skill, and the
|
|
@@ -327,7 +327,7 @@ ${consoleShellScript}
|
|
|
327
327
|
|
|
328
328
|
// ---- 3. Done ----
|
|
329
329
|
function renderDone(c) {
|
|
330
|
-
var agentHref = '/agents/config?agent=' + encodeURIComponent(c.agent);
|
|
330
|
+
var agentHref = '/agents/config?agent=' + encodeURIComponent(c.agent) + '&tab=workflows';
|
|
331
331
|
var items = c.checklist || [];
|
|
332
332
|
page.innerHTML = '<div class="wf-card">' +
|
|
333
333
|
'<h2>' + esc(c.workflow) + ' is ready</h2>' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"dist/core/trusted-proxy.d.ts",
|
|
47
47
|
"dist/core/usage.js",
|
|
48
48
|
"dist/core/usage.d.ts",
|
|
49
|
+
"dist/core/run-events.js",
|
|
50
|
+
"dist/core/run-events.d.ts",
|
|
49
51
|
"dist/core/email-senders.js",
|
|
50
52
|
"dist/core/email-senders.d.ts",
|
|
51
53
|
"dist/core/notifier-file.js",
|