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.
@@ -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
- export function createScheduleTools(agentName) {
29
- // Read once per run: the time zone is in the tool description, which
30
- // should stay the same from call to call.
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. 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).',
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: ['message', 'schedule'],
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: {
@@ -1,26 +1,55 @@
1
- import type { WorkflowStep } from './system-agents/workflow-planner.js';
2
- export interface WorkflowRecord {
3
- /** lowercase-hyphenated; also its skill's folder name. */
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: string;
6
- summary: string;
7
- trigger: {
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
- /** Its schedule's name, when it has one (the Schedules tab's task_name). */
15
- scheduleName?: string;
16
- /** What the person wrote, kept for reference. */
17
- description?: string;
18
- createdAt: string;
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 function workflowsPath(agentDir: string): string;
21
- /** The agent's workflows; none when it has no workflows.yml or it can't be read. */
22
- export declare function readWorkflows(agentDir: string): WorkflowRecord[];
23
- /** Adds a workflow, replacing one with the same id. */
24
- export declare function saveWorkflow(agentDir: string, workflow: WorkflowRecord): void;
25
- /** Forgets a workflow. Its skill, schedule and rules are left as they are. */
26
- export declare function removeWorkflow(agentDir: string, id: string): void;
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
+ }[];
@@ -1,39 +1,230 @@
1
- // An agent's workflows: jobs it does, each a skill (its steps), a trigger
2
- // (a schedule, or a message) and checkpoints (questions, and tools set to
3
- // "ask"). Recorded in agents/<name>/workflows.yml when one is added from
4
- // "Describe a workflow", so the agent's Overview can list them; the skill,
5
- // the schedule and the rules themselves live where they always do.
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { join } from 'node:path';
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
- export function workflowsPath(agentDir) {
10
- return join(agentDir, 'workflows.yml');
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
- /** The agent's workflows; none when it has no workflows.yml or it can't be read. */
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 path = workflowsPath(agentDir);
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
- return Array.isArray(parsed?.workflows) ? parsed.workflows.filter((w) => w && typeof w.id === 'string' && typeof w.name === 'string') : [];
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
- /** Adds a workflow, replacing one with the same id. */
26
- export function saveWorkflow(agentDir, workflow) {
27
- const workflows = readWorkflows(agentDir).filter((w) => w.id !== workflow.id);
28
- workflows.push(workflow);
29
- const header = "# This agent's workflows, added from the console's \"Describe a workflow\". Each one's\n# steps are its skill (skills/<id>/SKILL.md); its schedule and rules are in the Schedules and Permissions tabs.\n";
30
- writeFileSync(workflowsPath(agentDir), header + stringifyYaml({ workflows }));
31
- }
32
- /** Forgets a workflow. Its skill, schedule and rules are left as they are. */
33
- export function removeWorkflow(agentDir, id) {
34
- const workflows = readWorkflows(agentDir);
35
- if (!workflows.some((w) => w.id === id))
36
- return;
37
- const rest = workflows.filter((w) => w.id !== id);
38
- writeFileSync(workflowsPath(agentDir), stringifyYaml({ workflows: rest }));
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, runWorkflowNow, 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, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate, type WorkflowPlan, type WorkflowPlannerTools, type WorkflowStep } from './core/system-agents/workflow-planner.js';
86
- export { readWorkflows, removeWorkflow, saveWorkflow, type WorkflowRecord } from './core/workflows.js';
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, runWorkflowNow, startPlanJob, resolveMcpServers, prepareMcpAbilities, } from './web/workflow-admin.js';
78
- export { WORKFLOW_PLANNER_PROMPT, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate } from './core/system-agents/workflow-planner.js';
79
- export { readWorkflows, removeWorkflow, saveWorkflow } from './core/workflows.js';
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' variant carries this (see its own
182
- // doc comment) — refusing rather than silently dropping it for
183
- // another provider, same "don't guess" posture the model-name check
184
- // right above already follows.
185
- if (reasoningEffort !== undefined && provider !== 'openai') {
186
- throw new AgentModelError(`model.reasoningEffort only applies to provider 'openai' — '${provider}' doesn't support it.`);
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