loopengine 0.2.1 → 0.3.0
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 +1 -1
- package/dist/core/agent-config.d.ts +4 -0
- package/dist/core/model-calls/anthropic-model-call.d.ts +7 -0
- package/dist/core/model-calls/anthropic-model-call.js +34 -5
- package/dist/core/model-calls/openai-model-call.js +2 -1
- package/dist/core/run-agent.js +11 -2
- package/dist/core/scheduler.d.ts +29 -1
- package/dist/core/scheduler.js +28 -4
- package/dist/core/skillgarden/loader.d.ts +5 -1
- package/dist/core/skillgarden/loader.js +10 -3
- package/dist/core/skillgarden/substitute.d.ts +15 -1
- package/dist/core/skillgarden/substitute.js +28 -6
- package/dist/core/system-agents/templates.d.ts +5 -0
- package/dist/core/system-agents/workflow-planner.d.ts +26 -1
- package/dist/core/system-agents/workflow-planner.js +57 -5
- package/dist/core/system-tools/schedule.d.ts +5 -1
- package/dist/core/system-tools/schedule.js +45 -10
- package/dist/core/workflows.d.ts +52 -23
- package/dist/core/workflows.js +218 -27
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/web/agent-file-admin.d.ts +2 -0
- package/dist/web/agent-file-admin.js +8 -6
- package/dist/web/agents-config-page.js +411 -24
- package/dist/web/skills-admin.d.ts +4 -8
- package/dist/web/skills-admin.js +8 -12
- package/dist/web/templates-admin.js +7 -2
- package/dist/web/workflow-admin.d.ts +43 -18
- package/dist/web/workflow-admin.js +117 -50
- package/dist/web/workflow-page.js +72 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cancelScheduledTask, createScheduledTask, getScheduledTask, listScheduledTasks, } from '../scheduler.js';
|
|
2
2
|
import { formatWallTime, workspaceTimeZone } from '../time-zone.js';
|
|
3
|
+
import { readWorkflows, validateParams } from '../workflows.js';
|
|
3
4
|
/** How many active (not done or cancelled) tasks one agent may create
|
|
4
5
|
* through these tools — each run is a model call, so a runaway loop of
|
|
5
6
|
* "schedule another one" stops here. The console and API aren't capped. */
|
|
@@ -16,7 +17,7 @@ function summary(t, timeZone) {
|
|
|
16
17
|
return {
|
|
17
18
|
task_id: t.task_id,
|
|
18
19
|
task_name: t.task_name,
|
|
19
|
-
message: t.message,
|
|
20
|
+
...(t.workflow_id ? { workflow_id: t.workflow_id, ...(t.params ? { params: t.params } : {}) } : { message: t.message }),
|
|
20
21
|
schedule: t.schedule,
|
|
21
22
|
status: t.status,
|
|
22
23
|
next_run_at: t.next_run_at,
|
|
@@ -25,10 +26,18 @@ function summary(t, timeZone) {
|
|
|
25
26
|
last_status: t.last_status,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/** `agentDir`: the agent's folder, where its workflows are listed
|
|
30
|
+
* (workflows.yml). Without it the agent can only schedule messages. */
|
|
31
|
+
export function createScheduleTools(agentName, options = {}) {
|
|
32
|
+
// Read once per run: the time zone and the workflows are in the tool
|
|
33
|
+
// description, which should stay the same from call to call.
|
|
31
34
|
const timeZone = workspaceTimeZone();
|
|
35
|
+
const workflows = options.agentDir ? readWorkflows(options.agentDir) : [];
|
|
36
|
+
const paramList = (w) => {
|
|
37
|
+
const names = Object.entries(w.parameters).map(([name, p]) => `${name}${p.required ? '' : '?'}`);
|
|
38
|
+
return names.length ? `; params: ${names.join(', ')}` : '';
|
|
39
|
+
};
|
|
40
|
+
const workflowList = workflows.map((w) => `"${w.id}" (${w.title}: ${w.description}${paramList(w)})`).join('; ');
|
|
32
41
|
async function ownTask(taskId, tool) {
|
|
33
42
|
const task = await getScheduledTask(taskId);
|
|
34
43
|
// Another agent's task reads exactly like a missing one.
|
|
@@ -38,14 +47,24 @@ export function createScheduleTools(agentName) {
|
|
|
38
47
|
}
|
|
39
48
|
const scheduleTask = {
|
|
40
49
|
name: 'system_schedule_task',
|
|
41
|
-
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job.
|
|
50
|
+
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job.' +
|
|
51
|
+
(workflows.length
|
|
52
|
+
? ' To run one of your workflows on a schedule, give its workflow_id instead of a message, and its params (names ending in ? are optional): each run then starts with that workflow\'s start message and steps as they are at the time, so later edits to the workflow apply. Your workflows: ' + workflowList + '.'
|
|
53
|
+
: '') +
|
|
54
|
+
' When it fires you receive the message as a new conversation (none of the current one carries over, so make the message self-contained) and handle it with your tools as usual, with no person live. A tool call that needs approval goes wherever your approval requests are sent and waits; if nowhere is set, it is denied. Use only when the user asks for something to happen later or regularly, never on your own initiative. Times are in this workspace\'s time zone, ' + timeZone + ', unless you give an offset, so when the user means another time zone, give its offset. Prefer the longest interval that serves them since each run is a model call, and confirm the schedule back in plain words. Returns task_id, next_run_at and next_run_local (the workspace\'s time).',
|
|
42
55
|
input_schema: {
|
|
43
56
|
type: 'object',
|
|
44
57
|
properties: {
|
|
45
58
|
message: {
|
|
46
59
|
type: 'string',
|
|
47
|
-
description: 'What you will receive when the task fires, written as an instruction to your future self with every detail it needs, e.g. "Check whether order 4417 has shipped and email the customer at a@example.com if it has."',
|
|
60
|
+
description: 'What you will receive when the task fires, written as an instruction to your future self with every detail it needs, e.g. "Check whether order 4417 has shipped and email the customer at a@example.com if it has." Leave it out when giving workflow_id.',
|
|
48
61
|
},
|
|
62
|
+
...(workflows.length
|
|
63
|
+
? {
|
|
64
|
+
workflow_id: { type: 'string', enum: workflows.map((w) => w.id), description: 'One of your workflows, to run it on this schedule instead of a message.' },
|
|
65
|
+
params: { type: 'object', description: 'The workflow\'s parameters for every run, e.g. { "region": "US" }.', additionalProperties: { type: 'string' } },
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
49
68
|
schedule: {
|
|
50
69
|
type: 'object',
|
|
51
70
|
description: 'Either { "run_at": "<ISO 8601 timestamp>" } for once, or { "every": "<interval>", ... } to repeat until cancelled — a number plus s/m/h/d, e.g. "30m", "1h", "1d"; 30s at the shortest. A timestamp without an offset, like "2026-11-02T09:00", is in the workspace\'s time zone (' + timeZone + '). A repeating task keeps its first slot: started at 10:07 with "1h", it fires at 11:07, 12:07, ... For a time of day or day of week, add "at" (24-hour "HH:MM") and for weekly "weekday": "every day at 9am" is { "every": "1d", "at": "09:00" }; "every Monday at 9am" is { "every": "7d", "at": "09:00", "weekday": "monday" }. Daily and weekly tasks keep their time of day when the clocks change. Or give "start_at", a timestamp, for the first run.',
|
|
@@ -54,26 +73,42 @@ export function createScheduleTools(agentName) {
|
|
|
54
73
|
task_name: { type: 'string', description: 'Optional short label, shown when listing tasks and in the console.' },
|
|
55
74
|
notify: { type: 'boolean', description: 'Optional — also send each run\'s result through your notifier (where your approval requests go), so a person sees it.' },
|
|
56
75
|
},
|
|
57
|
-
required: ['
|
|
76
|
+
required: ['schedule'],
|
|
58
77
|
},
|
|
59
78
|
execute: async (input) => {
|
|
60
79
|
const active = (await listScheduledTasks({ agent: agentName })).filter((t) => t.status !== 'done' && t.status !== 'cancelled');
|
|
61
80
|
if (active.length >= maxActivePerAgent()) {
|
|
62
81
|
throw new Error(`system_schedule_task: you already have ${active.length} active scheduled tasks (the limit is ${maxActivePerAgent()}); cancel one you no longer need first`);
|
|
63
82
|
}
|
|
83
|
+
const workflowId = typeof input.workflow_id === 'string' && input.workflow_id ? input.workflow_id : undefined;
|
|
84
|
+
const workflow = workflowId ? workflows.find((w) => w.id === workflowId) : undefined;
|
|
85
|
+
if (workflowId && !workflow) {
|
|
86
|
+
throw new Error(`system_schedule_task: you have no workflow "${workflowId}"${workflows.length ? `; yours are ${workflows.map((w) => `"${w.id}"`).join(', ')}` : ''}`);
|
|
87
|
+
}
|
|
88
|
+
let params;
|
|
89
|
+
if (workflow) {
|
|
90
|
+
try {
|
|
91
|
+
params = validateParams(workflow, input.params);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw new Error(`system_schedule_task: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
64
97
|
const task = await createScheduledTask({
|
|
65
98
|
agent: agentName,
|
|
66
|
-
message: String(input.message ?? ''),
|
|
99
|
+
message: workflowId ? undefined : String(input.message ?? ''),
|
|
100
|
+
workflow_id: workflowId,
|
|
101
|
+
...(params && Object.keys(params).length ? { params } : {}),
|
|
67
102
|
schedule: (input.schedule ?? {}),
|
|
68
103
|
task_name: typeof input.task_name === 'string' ? input.task_name : undefined,
|
|
69
104
|
notify: input.notify === true,
|
|
70
105
|
});
|
|
71
|
-
return JSON.stringify({ task_id: task.task_id, next_run_at: task.next_run_at, next_run_local: nextRunLocal(task, timeZone), schedule: task.schedule });
|
|
106
|
+
return JSON.stringify({ task_id: task.task_id, ...(workflowId ? { workflow_id: workflowId } : {}), next_run_at: task.next_run_at, next_run_local: nextRunLocal(task, timeZone), schedule: task.schedule });
|
|
72
107
|
},
|
|
73
108
|
};
|
|
74
109
|
const listTasks = {
|
|
75
110
|
name: 'system_list_scheduled_tasks',
|
|
76
|
-
description: 'List your own scheduled tasks (task_id, name, message, schedule, status, next_run_at, run_count, last_status). Check this before scheduling something recurring, so you don\'t create a duplicate. status "awaiting_approval" means a run is waiting on someone to approve or answer.',
|
|
111
|
+
description: 'List your own scheduled tasks (task_id, name, message or workflow_id, schedule, status, next_run_at, run_count, last_status). Check this before scheduling something recurring, so you don\'t create a duplicate. status "awaiting_approval" means a run is waiting on someone to approve or answer.',
|
|
77
112
|
input_schema: {
|
|
78
113
|
type: 'object',
|
|
79
114
|
properties: {
|
package/dist/core/workflows.d.ts
CHANGED
|
@@ -1,26 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export interface WorkflowParameter {
|
|
2
|
+
description: string;
|
|
3
|
+
required: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface Workflow {
|
|
6
|
+
/** The skill's id (its folder name). */
|
|
4
7
|
id: string;
|
|
5
|
-
name
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
kind: 'schedule' | 'on_request';
|
|
9
|
-
description: string;
|
|
10
|
-
};
|
|
11
|
-
steps: WorkflowStep[];
|
|
12
|
-
/** The message each run starts with. */
|
|
8
|
+
/** A name for people, e.g. "Refund check". */
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
13
11
|
startMessage: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
/** When it runs, in plain words: "Every Monday at 9", "When asked". */
|
|
13
|
+
trigger: string;
|
|
14
|
+
parameters: Record<string, WorkflowParameter>;
|
|
15
|
+
/** Its steps: the skill body, placeholders unfilled. */
|
|
16
|
+
steps: string;
|
|
17
|
+
}
|
|
18
|
+
/** The front matter that makes a skill a workflow. */
|
|
19
|
+
export interface WorkflowFields {
|
|
20
|
+
title?: string;
|
|
21
|
+
startMessage: string;
|
|
22
|
+
trigger?: string;
|
|
23
|
+
parameters?: Record<string, WorkflowParameter>;
|
|
19
24
|
}
|
|
20
|
-
export declare
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export declare function
|
|
25
|
-
/**
|
|
26
|
-
export declare function
|
|
25
|
+
export declare class WorkflowParamsError extends Error {
|
|
26
|
+
}
|
|
27
|
+
/** Writes a skill file: its front matter (name and description first) and
|
|
28
|
+
* body. */
|
|
29
|
+
export declare function writeSkillFile(path: string, frontmatter: Record<string, unknown>, body: string): void;
|
|
30
|
+
/** The agent's workflows: its skills that have a start message. */
|
|
31
|
+
export declare function readWorkflows(agentDir: string): Workflow[];
|
|
32
|
+
export declare function readWorkflow(agentDir: string, id: string): Workflow | undefined;
|
|
33
|
+
/** Makes a skill a workflow, or changes how its runs start. The skill must
|
|
34
|
+
* exist; its body and other front matter are kept. */
|
|
35
|
+
export declare function setWorkflowFields(agentDir: string, id: string, fields: WorkflowFields): void;
|
|
36
|
+
/** Stops a skill being a workflow. The skill itself stays. */
|
|
37
|
+
export declare function clearWorkflowFields(agentDir: string, id: string): void;
|
|
38
|
+
/** Checks a run's parameters against what the workflow declares: every
|
|
39
|
+
* required one given, no unknown ones. Values are given as text (numbers
|
|
40
|
+
* and true/false are accepted and turned into text). */
|
|
41
|
+
export declare function validateParams(workflow: Workflow, input: unknown): Record<string, string>;
|
|
42
|
+
/** The message a run of the workflow starts with: its start message, then
|
|
43
|
+
* its steps, each $parameter filled in. Values no placeholder uses are
|
|
44
|
+
* listed as inputs, so the agent still gets them. */
|
|
45
|
+
export declare function workflowRunMessage(workflow: Workflow, params?: Record<string, string>): string;
|
|
46
|
+
export declare function legacyWorkflowsPath(agentDir: string): string;
|
|
47
|
+
/** Moves the workflows in an agent's workflows.yml into their skills'
|
|
48
|
+
* front matter, then renames the file to workflows.migrated.yml. Returns
|
|
49
|
+
* each moved workflow's id with the schedule name it was linked by, so
|
|
50
|
+
* the caller can point those schedules at the workflow. Does nothing
|
|
51
|
+
* when there is no workflows.yml. */
|
|
52
|
+
export declare function migrateWorkflowsFile(agentDir: string): {
|
|
53
|
+
id: string;
|
|
54
|
+
scheduleName?: string;
|
|
55
|
+
}[];
|
package/dist/core/workflows.js
CHANGED
|
@@ -1,39 +1,230 @@
|
|
|
1
|
-
// An agent's workflows:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// An agent's workflows: skills it can be told to run, by hand, from an app
|
|
2
|
+
// over the API, or on a schedule. A workflow is a skill
|
|
3
|
+
// (agents/<name>/skills/<id>/SKILL.md) whose front matter also says how a
|
|
4
|
+
// run starts:
|
|
5
|
+
//
|
|
6
|
+
// ---
|
|
7
|
+
// name: refund-check
|
|
8
|
+
// description: Check a refund request against the order and our policy
|
|
9
|
+
// title: Refund check
|
|
10
|
+
// start_message: Check the refund request for order $order_number.
|
|
11
|
+
// trigger: When a refund request comes in
|
|
12
|
+
// parameters:
|
|
13
|
+
// order_number: { description: The order to check, required: true }
|
|
14
|
+
// customer_note: { description: What the customer wrote }
|
|
15
|
+
// ---
|
|
16
|
+
// 1. Look up order $order_number…
|
|
17
|
+
//
|
|
18
|
+
// The body is its steps. A run starts with the start message and those
|
|
19
|
+
// steps, with each $parameter filled in, so the model has them from its
|
|
20
|
+
// first turn rather than picking the skill from its index. None of these
|
|
21
|
+
// keys reach the model otherwise: the skill index shows only a skill's
|
|
22
|
+
// name and description (core/skillgarden).
|
|
23
|
+
//
|
|
24
|
+
// Agents from before kept their workflows in workflows.yml;
|
|
25
|
+
// migrateWorkflowsFile moves those into their skills.
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
8
28
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
9
|
-
|
|
10
|
-
|
|
29
|
+
import { parseSkillFile, substituteParameters } from './skillgarden/index.js';
|
|
30
|
+
export class WorkflowParamsError extends Error {
|
|
31
|
+
}
|
|
32
|
+
const PARAMETER_NAME = /^[a-z_][a-z0-9_]*$/;
|
|
33
|
+
const WORKFLOW_KEYS = ['title', 'start_message', 'trigger', 'parameters'];
|
|
34
|
+
function skillsDir(agentDir) {
|
|
35
|
+
return join(agentDir, 'skills');
|
|
36
|
+
}
|
|
37
|
+
function skillPath(agentDir, id) {
|
|
38
|
+
return join(skillsDir(agentDir), id, 'SKILL.md');
|
|
39
|
+
}
|
|
40
|
+
function readSkillFile(path, id) {
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const { frontmatter, body } = parseSkillFile(readFileSync(path, 'utf8'), id);
|
|
45
|
+
return { frontmatter: frontmatter, body };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
11
50
|
}
|
|
12
|
-
/**
|
|
51
|
+
/** Writes a skill file: its front matter (name and description first) and
|
|
52
|
+
* body. */
|
|
53
|
+
export function writeSkillFile(path, frontmatter, body) {
|
|
54
|
+
const { name, description, ...rest } = frontmatter;
|
|
55
|
+
const ordered = { name, description, ...rest };
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
writeFileSync(path, `---\n${stringifyYaml(ordered).trimEnd()}\n---\n\n${body.trim()}\n`);
|
|
58
|
+
}
|
|
59
|
+
function titleFromId(id) {
|
|
60
|
+
const words = id.replace(/-/g, ' ');
|
|
61
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
62
|
+
}
|
|
63
|
+
function parametersFrom(value) {
|
|
64
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
65
|
+
return {};
|
|
66
|
+
const parameters = {};
|
|
67
|
+
for (const [name, spec] of Object.entries(value)) {
|
|
68
|
+
if (!PARAMETER_NAME.test(name))
|
|
69
|
+
continue;
|
|
70
|
+
const s = spec && typeof spec === 'object' ? spec : {};
|
|
71
|
+
parameters[name] = { description: typeof s.description === 'string' ? s.description : '', required: s.required === true };
|
|
72
|
+
}
|
|
73
|
+
return parameters;
|
|
74
|
+
}
|
|
75
|
+
function workflowFrom(id, frontmatter, body) {
|
|
76
|
+
const startMessage = typeof frontmatter.start_message === 'string' ? frontmatter.start_message.trim() : '';
|
|
77
|
+
if (!startMessage)
|
|
78
|
+
return undefined;
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
title: typeof frontmatter.title === 'string' && frontmatter.title.trim() ? frontmatter.title.trim() : titleFromId(id),
|
|
82
|
+
description: typeof frontmatter.description === 'string' ? frontmatter.description : '',
|
|
83
|
+
startMessage,
|
|
84
|
+
trigger: typeof frontmatter.trigger === 'string' && frontmatter.trigger.trim() ? frontmatter.trigger.trim() : 'When asked',
|
|
85
|
+
parameters: parametersFrom(frontmatter.parameters),
|
|
86
|
+
steps: body,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** The agent's workflows: its skills that have a start message. */
|
|
13
90
|
export function readWorkflows(agentDir) {
|
|
14
|
-
const
|
|
91
|
+
const dir = skillsDir(agentDir);
|
|
92
|
+
if (!existsSync(dir))
|
|
93
|
+
return [];
|
|
94
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
95
|
+
.filter((entry) => entry.isDirectory())
|
|
96
|
+
.map((entry) => entry.name)
|
|
97
|
+
.sort()
|
|
98
|
+
.flatMap((id) => {
|
|
99
|
+
const skill = readSkillFile(skillPath(agentDir, id), id);
|
|
100
|
+
const workflow = skill && workflowFrom(id, skill.frontmatter, skill.body);
|
|
101
|
+
return workflow ? [workflow] : [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function readWorkflow(agentDir, id) {
|
|
105
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id))
|
|
106
|
+
return undefined;
|
|
107
|
+
const skill = readSkillFile(skillPath(agentDir, id), id);
|
|
108
|
+
return skill && workflowFrom(id, skill.frontmatter, skill.body);
|
|
109
|
+
}
|
|
110
|
+
/** Makes a skill a workflow, or changes how its runs start. The skill must
|
|
111
|
+
* exist; its body and other front matter are kept. */
|
|
112
|
+
export function setWorkflowFields(agentDir, id, fields) {
|
|
113
|
+
const path = skillPath(agentDir, id);
|
|
114
|
+
const skill = readSkillFile(path, id);
|
|
115
|
+
if (!skill)
|
|
116
|
+
throw new Error(`No skill "${id}" to make a workflow.`);
|
|
117
|
+
for (const name of Object.keys(fields.parameters ?? {})) {
|
|
118
|
+
if (!PARAMETER_NAME.test(name))
|
|
119
|
+
throw new Error(`Parameter "${name}": use lowercase letters, digits and _, starting with a letter, e.g. "order_number".`);
|
|
120
|
+
}
|
|
121
|
+
const frontmatter = { ...skill.frontmatter };
|
|
122
|
+
for (const key of WORKFLOW_KEYS)
|
|
123
|
+
delete frontmatter[key];
|
|
124
|
+
writeSkillFile(path, {
|
|
125
|
+
...frontmatter,
|
|
126
|
+
...(fields.title ? { title: fields.title } : {}),
|
|
127
|
+
start_message: fields.startMessage,
|
|
128
|
+
...(fields.trigger ? { trigger: fields.trigger } : {}),
|
|
129
|
+
...(fields.parameters && Object.keys(fields.parameters).length ? { parameters: fields.parameters } : {}),
|
|
130
|
+
}, skill.body);
|
|
131
|
+
}
|
|
132
|
+
/** Stops a skill being a workflow. The skill itself stays. */
|
|
133
|
+
export function clearWorkflowFields(agentDir, id) {
|
|
134
|
+
const path = skillPath(agentDir, id);
|
|
135
|
+
const skill = readSkillFile(path, id);
|
|
136
|
+
if (!skill)
|
|
137
|
+
return;
|
|
138
|
+
const frontmatter = { ...skill.frontmatter };
|
|
139
|
+
for (const key of WORKFLOW_KEYS)
|
|
140
|
+
delete frontmatter[key];
|
|
141
|
+
writeSkillFile(path, frontmatter, skill.body);
|
|
142
|
+
}
|
|
143
|
+
/** Checks a run's parameters against what the workflow declares: every
|
|
144
|
+
* required one given, no unknown ones. Values are given as text (numbers
|
|
145
|
+
* and true/false are accepted and turned into text). */
|
|
146
|
+
export function validateParams(workflow, input) {
|
|
147
|
+
if (input !== undefined && input !== null && (typeof input !== 'object' || Array.isArray(input))) {
|
|
148
|
+
throw new WorkflowParamsError('params must be an object of names to values, e.g. { "order_number": "EM20-123" }');
|
|
149
|
+
}
|
|
150
|
+
const given = (input ?? {});
|
|
151
|
+
const params = {};
|
|
152
|
+
const problems = [];
|
|
153
|
+
for (const [name, value] of Object.entries(given)) {
|
|
154
|
+
if (!(name in workflow.parameters)) {
|
|
155
|
+
problems.push(`unknown parameter "${name}"`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (value === undefined || value === null || value === '')
|
|
159
|
+
continue;
|
|
160
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
161
|
+
problems.push(`"${name}" must be text`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
params[name] = String(value);
|
|
165
|
+
}
|
|
166
|
+
for (const [name, spec] of Object.entries(workflow.parameters)) {
|
|
167
|
+
if (spec.required && !params[name]?.trim())
|
|
168
|
+
problems.push(`"${name}" is required`);
|
|
169
|
+
}
|
|
170
|
+
if (problems.length) {
|
|
171
|
+
const known = Object.keys(workflow.parameters);
|
|
172
|
+
throw new WorkflowParamsError(`${workflow.title}: ${problems.join('; ')}${known.length ? ` (its parameters: ${known.join(', ')})` : ' (it takes no parameters)'}`);
|
|
173
|
+
}
|
|
174
|
+
return params;
|
|
175
|
+
}
|
|
176
|
+
/** The message a run of the workflow starts with: its start message, then
|
|
177
|
+
* its steps, each $parameter filled in. Values no placeholder uses are
|
|
178
|
+
* listed as inputs, so the agent still gets them. */
|
|
179
|
+
export function workflowRunMessage(workflow, params = {}) {
|
|
180
|
+
const used = new Set();
|
|
181
|
+
const declared = Object.keys(workflow.parameters);
|
|
182
|
+
// Named placeholders only: positional $1-style filling would eat dollar
|
|
183
|
+
// amounts like "$50" in the steps.
|
|
184
|
+
const fill = (text) => substituteParameters(text, params, { declared, used });
|
|
185
|
+
const start = fill(workflow.startMessage);
|
|
186
|
+
const steps = fill(workflow.steps).trim();
|
|
187
|
+
const unused = Object.entries(params).filter(([name]) => !used.has(name));
|
|
188
|
+
return [
|
|
189
|
+
start,
|
|
190
|
+
unused.length ? `Inputs:\n${unused.map(([name, value]) => `- ${name}: ${value}`).join('\n')}` : '',
|
|
191
|
+
steps ? `These are the steps, from your "${workflow.id}" skill:\n\n${steps}` : '',
|
|
192
|
+
].filter(Boolean).join('\n\n');
|
|
193
|
+
}
|
|
194
|
+
export function legacyWorkflowsPath(agentDir) {
|
|
195
|
+
return join(agentDir, 'workflows.yml');
|
|
196
|
+
}
|
|
197
|
+
/** Moves the workflows in an agent's workflows.yml into their skills'
|
|
198
|
+
* front matter, then renames the file to workflows.migrated.yml. Returns
|
|
199
|
+
* each moved workflow's id with the schedule name it was linked by, so
|
|
200
|
+
* the caller can point those schedules at the workflow. Does nothing
|
|
201
|
+
* when there is no workflows.yml. */
|
|
202
|
+
export function migrateWorkflowsFile(agentDir) {
|
|
203
|
+
const path = legacyWorkflowsPath(agentDir);
|
|
15
204
|
if (!existsSync(path))
|
|
16
205
|
return [];
|
|
206
|
+
let records = [];
|
|
17
207
|
try {
|
|
18
208
|
const parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
19
|
-
|
|
209
|
+
records = Array.isArray(parsed?.workflows) ? parsed.workflows.filter((w) => w && typeof w.id === 'string') : [];
|
|
20
210
|
}
|
|
21
211
|
catch {
|
|
22
212
|
return [];
|
|
23
213
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
214
|
+
const moved = [];
|
|
215
|
+
for (const record of records) {
|
|
216
|
+
const skill = readSkillFile(skillPath(agentDir, record.id), record.id);
|
|
217
|
+
if (!skill || !record.startMessage?.trim())
|
|
218
|
+
continue;
|
|
219
|
+
if (!workflowFrom(record.id, skill.frontmatter, skill.body)) {
|
|
220
|
+
setWorkflowFields(agentDir, record.id, {
|
|
221
|
+
...(record.name ? { title: record.name } : {}),
|
|
222
|
+
startMessage: record.startMessage,
|
|
223
|
+
...(record.trigger?.description ? { trigger: record.trigger.description } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
moved.push({ id: record.id, ...(record.scheduleName ? { scheduleName: record.scheduleName } : {}) });
|
|
227
|
+
}
|
|
228
|
+
renameSync(path, join(agentDir, 'workflows.migrated.yml'));
|
|
229
|
+
return moved;
|
|
39
230
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { setUsageSink, recordUsage, type UsageEvent, type UsageSink } from '#cor
|
|
|
10
10
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS, type NotifierFile, type MessagingApprover } from '#core/notifier-file.js';
|
|
11
11
|
export { listDeliveries, recordDelivery, type NotifierDelivery } from '#core/notifier-deliveries.js';
|
|
12
12
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
13
|
-
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
13
|
+
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, setScheduledTaskNotify, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
14
14
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
15
15
|
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
16
16
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
@@ -81,9 +81,12 @@ export { templatesPageHtml } from './web/templates-page.js';
|
|
|
81
81
|
export { TemplateError, createFromTemplate, createFromTemplateObject, describeTemplateSchedule, listTemplates, previewTemplate, templateChecklist, type CreatedTemplateAgent, type LegacyTemplateCreateDeps, type TemplateChecklistItem, type TemplateCreateDeps, type TemplatePreview, type TemplateSummary, } from './web/templates-admin.js';
|
|
82
82
|
export { STARTER_TEMPLATES, starterTemplate, templateScheduleStart, type StarterTemplate, type TemplateRule, type TemplateSchedule, type TemplateSetup, type TemplateSkill, } from './core/system-agents/templates.js';
|
|
83
83
|
export { workflowPageHtml } from './web/workflow-page.js';
|
|
84
|
-
export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections,
|
|
85
|
-
export { WORKFLOW_PLANNER_PROMPT, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate, type WorkflowPlan, type WorkflowPlannerTools, type WorkflowStep } from './core/system-agents/workflow-planner.js';
|
|
86
|
-
export {
|
|
84
|
+
export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, migrateAgentWorkflows, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, prepareWorkflowRun, updateWorkflow, startPlanJob, type AgentWorkflowView, type CreatedWorkflow, type PlanJob, type PlannedWorkflow, type WorkflowAgentInfo, type WorkflowCheck, type WorkflowConflict, type WorkflowCreateDeps, type WorkflowMcpDeps, type WorkflowMcpCreateDeps, resolveMcpServers, prepareMcpAbilities, } from './web/workflow-admin.js';
|
|
85
|
+
export { WORKFLOW_PLANNER_PROMPT, planParameters, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate, type WorkflowPlan, type WorkflowPlannerTools, type WorkflowStep } from './core/system-agents/workflow-planner.js';
|
|
86
|
+
export { createTrigger, updateTrigger, rotateTrigger, listTriggers, findTriggerByToken, publicTrigger, checkTriggerSettings, verifySenderSignature, parsePayload, valueAt, deliveryId, passesFilter, paramsFromPayload, recordDelivery as recordTriggerDelivery, FileTriggerStore, getTriggerStore, setTriggerStore, TriggerError, type WorkflowTrigger, type PublicTrigger, type TriggerSettings, type TriggerVerify, type TriggerFilter, type TriggerSender, type SignatureAlgorithm, type TriggerDelivery, type TriggerOutcome, type TriggerStore } from './core/workflow-triggers.js';
|
|
87
|
+
export { checkCallbackUrl, callbackSecret, signCallback, deliverCallback, runCallbackPayload, isPrivateAddress, CallbackUrlError, type CallbackDelivery, type RunCallbackEvent } from './core/run-callbacks.js';
|
|
88
|
+
export { WorkflowRunner, FileRunStore, getRunStore, setRunStore, listRuns, getRun, isUnderWay, RunError, type WorkflowRun, type WorkflowRunStatus, type RunStore, type ExecuteRun, type RunOutcome, type StartRunInput } from './core/workflow-runs.js';
|
|
89
|
+
export { readWorkflows, readWorkflow, setWorkflowFields, clearWorkflowFields, validateParams, workflowRunMessage, migrateWorkflowsFile, WorkflowParamsError, type Workflow, type WorkflowFields, type WorkflowParameter } from './core/workflows.js';
|
|
87
90
|
export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
88
91
|
export { playgroundHtml } from './web/playground.js';
|
|
89
92
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { setUsageSink, recordUsage } from '#core/usage.js';
|
|
|
12
12
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS } from '#core/notifier-file.js';
|
|
13
13
|
export { listDeliveries, recordDelivery } from '#core/notifier-deliveries.js';
|
|
14
14
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
15
|
-
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
15
|
+
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, setScheduledTaskNotify, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
16
16
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
17
17
|
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval } from '#core/messaging/engine.js';
|
|
18
18
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
@@ -74,9 +74,12 @@ export { templatesPageHtml } from './web/templates-page.js';
|
|
|
74
74
|
export { TemplateError, createFromTemplate, createFromTemplateObject, describeTemplateSchedule, listTemplates, previewTemplate, templateChecklist, } from './web/templates-admin.js';
|
|
75
75
|
export { STARTER_TEMPLATES, starterTemplate, templateScheduleStart, } from './core/system-agents/templates.js';
|
|
76
76
|
export { workflowPageHtml } from './web/workflow-page.js';
|
|
77
|
-
export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections,
|
|
78
|
-
export { WORKFLOW_PLANNER_PROMPT, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate } from './core/system-agents/workflow-planner.js';
|
|
79
|
-
export {
|
|
77
|
+
export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, migrateAgentWorkflows, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, prepareWorkflowRun, updateWorkflow, startPlanJob, resolveMcpServers, prepareMcpAbilities, } from './web/workflow-admin.js';
|
|
78
|
+
export { WORKFLOW_PLANNER_PROMPT, planParameters, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate } from './core/system-agents/workflow-planner.js';
|
|
79
|
+
export { createTrigger, updateTrigger, rotateTrigger, listTriggers, findTriggerByToken, publicTrigger, checkTriggerSettings, verifySenderSignature, parsePayload, valueAt, deliveryId, passesFilter, paramsFromPayload, recordDelivery as recordTriggerDelivery, FileTriggerStore, getTriggerStore, setTriggerStore, TriggerError } from './core/workflow-triggers.js';
|
|
80
|
+
export { checkCallbackUrl, callbackSecret, signCallback, deliverCallback, runCallbackPayload, isPrivateAddress, CallbackUrlError } from './core/run-callbacks.js';
|
|
81
|
+
export { WorkflowRunner, FileRunStore, getRunStore, setRunStore, listRuns, getRun, isUnderWay, RunError } from './core/workflow-runs.js';
|
|
82
|
+
export { readWorkflows, readWorkflow, setWorkflowFields, clearWorkflowFields, validateParams, workflowRunMessage, migrateWorkflowsFile, WorkflowParamsError } from './core/workflows.js';
|
|
80
83
|
export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
81
84
|
export { playgroundHtml } from './web/playground.js';
|
|
82
85
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
|
@@ -3,6 +3,8 @@ export declare class AgentEditNotSupportedError extends Error {
|
|
|
3
3
|
export declare class AgentFileNotFoundError extends Error {
|
|
4
4
|
}
|
|
5
5
|
type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini' | 'xai' | 'meta';
|
|
6
|
+
/** The providers whose model calls take a reasoning effort. */
|
|
7
|
+
export declare const REASONING_PROVIDERS: Provider[];
|
|
6
8
|
export interface AgentEditableFields {
|
|
7
9
|
systemPrompt?: string;
|
|
8
10
|
model?: {
|
|
@@ -48,6 +48,8 @@ export class AgentEditNotSupportedError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
export class AgentFileNotFoundError extends Error {
|
|
50
50
|
}
|
|
51
|
+
/** The providers whose model calls take a reasoning effort. */
|
|
52
|
+
export const REASONING_PROVIDERS = ['openai', 'anthropic'];
|
|
51
53
|
const MODEL_ENV_VAR = {
|
|
52
54
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
53
55
|
openai: 'OPENAI_API_KEY',
|
|
@@ -178,12 +180,12 @@ export function editAgentFile(agentName, fields) {
|
|
|
178
180
|
throw new AgentEditNotSupportedError('model.maxTokens must be a positive integer.');
|
|
179
181
|
}
|
|
180
182
|
const reasoningEffort = fields.model.reasoningEffort;
|
|
181
|
-
// Only AgentModelConfig's 'openai'
|
|
182
|
-
// doc
|
|
183
|
-
// another provider, same "don't guess" posture the model-name
|
|
184
|
-
// right above already follows.
|
|
185
|
-
if (reasoningEffort !== undefined && provider
|
|
186
|
-
throw new AgentModelError(`model.reasoningEffort only applies to
|
|
183
|
+
// Only AgentModelConfig's 'openai' and 'anthropic' variants carry this
|
|
184
|
+
// (see their doc comments) — refusing rather than silently dropping it
|
|
185
|
+
// for another provider, same "don't guess" posture the model-name
|
|
186
|
+
// check right above already follows.
|
|
187
|
+
if (reasoningEffort !== undefined && !REASONING_PROVIDERS.includes(provider)) {
|
|
188
|
+
throw new AgentModelError(`model.reasoningEffort only applies to providers ${REASONING_PROVIDERS.join(' and ')} — '${provider}' doesn't support it.`);
|
|
187
189
|
}
|
|
188
190
|
// Every field the form actually round-trips (not just provider/model)
|
|
189
191
|
// — omitting maxTokens/reasoningEffort here, the way this used to
|