@agentic-surfaces/core 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scheduler.d.ts +7 -0
- package/dist/scheduler.js +53 -12
- package/dist/schema.js +70 -20
- package/dist/types.d.ts +7 -1
- package/package.json +2 -2
package/dist/scheduler.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export declare class Scheduler {
|
|
|
30
30
|
/** When set + paused, scheduled cron fires are skipped (the platform-wide play/pause switch). */
|
|
31
31
|
pause?: PauseState | undefined);
|
|
32
32
|
add(workflow: Workflow): void;
|
|
33
|
+
/**
|
|
34
|
+
* Hot-reload the scheduled set in place: cancel every existing cron timer and re-register from
|
|
35
|
+
* `next`. Used by the server's /api/reload + fs.watch path so edited/added/removed cron schedules
|
|
36
|
+
* take effect without a process restart — no leaked timers, no double-firing. In-flight runs that
|
|
37
|
+
* were already enqueued are unaffected (the queue drains normally); only future fires change.
|
|
38
|
+
*/
|
|
39
|
+
reload(next: Workflow[]): void;
|
|
33
40
|
start(): void;
|
|
34
41
|
private enqueue;
|
|
35
42
|
private drain;
|
package/dist/scheduler.js
CHANGED
|
@@ -32,11 +32,18 @@ export function defaultRegistry() {
|
|
|
32
32
|
}
|
|
33
33
|
export async function runWorkflowOnce(workflow, opts) {
|
|
34
34
|
const registry = opts.registry ?? defaultRegistry();
|
|
35
|
-
const triggerNodeId = opts.triggerNodeId
|
|
36
|
-
|
|
35
|
+
const triggerNodeId = opts.triggerNodeId ??
|
|
36
|
+
workflow.nodes.find((n) => n.type.startsWith("trigger."))?.id;
|
|
37
37
|
if (!triggerNodeId)
|
|
38
38
|
throw new Error(`workflow ${workflow.name} has no trigger node`);
|
|
39
|
-
return runWorkflowExec({
|
|
39
|
+
return runWorkflowExec({
|
|
40
|
+
workflow,
|
|
41
|
+
triggerNodeId,
|
|
42
|
+
registry,
|
|
43
|
+
services: opts.services,
|
|
44
|
+
payload: opts.payload,
|
|
45
|
+
observer: opts.observer,
|
|
46
|
+
});
|
|
40
47
|
}
|
|
41
48
|
export function buildWorkflowRunner(opts) {
|
|
42
49
|
const { workflows, registry, observer, maxDepth = 10 } = opts;
|
|
@@ -49,8 +56,14 @@ export function buildWorkflowRunner(opts) {
|
|
|
49
56
|
throw new Error(`max workflow depth exceeded (possible cycle): depth=${depth}`);
|
|
50
57
|
depth++;
|
|
51
58
|
const subServices = { ...opts.services, runWorkflow };
|
|
52
|
-
return runWorkflowOnce(wf, {
|
|
53
|
-
|
|
59
|
+
return runWorkflowOnce(wf, {
|
|
60
|
+
registry,
|
|
61
|
+
services: subServices,
|
|
62
|
+
observer,
|
|
63
|
+
payload,
|
|
64
|
+
}).finally(() => {
|
|
65
|
+
depth--;
|
|
66
|
+
});
|
|
54
67
|
}
|
|
55
68
|
return runWorkflow;
|
|
56
69
|
}
|
|
@@ -71,7 +84,20 @@ export class Scheduler {
|
|
|
71
84
|
this.observer = observer;
|
|
72
85
|
this.pause = pause;
|
|
73
86
|
}
|
|
74
|
-
add(workflow) {
|
|
87
|
+
add(workflow) {
|
|
88
|
+
this.workflows.push(workflow);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Hot-reload the scheduled set in place: cancel every existing cron timer and re-register from
|
|
92
|
+
* `next`. Used by the server's /api/reload + fs.watch path so edited/added/removed cron schedules
|
|
93
|
+
* take effect without a process restart — no leaked timers, no double-firing. In-flight runs that
|
|
94
|
+
* were already enqueued are unaffected (the queue drains normally); only future fires change.
|
|
95
|
+
*/
|
|
96
|
+
reload(next) {
|
|
97
|
+
this.stop(); // cancels + clears all current Cron timers
|
|
98
|
+
this.workflows = [...next]; // replace the registered set
|
|
99
|
+
this.start(); // re-register timers from the new set
|
|
100
|
+
}
|
|
75
101
|
start() {
|
|
76
102
|
for (const wf of this.workflows) {
|
|
77
103
|
for (const node of wf.nodes) {
|
|
@@ -84,13 +110,19 @@ export class Scheduler {
|
|
|
84
110
|
this.services.logger?.info?.(`paused — skipping scheduled run of ${wf.name}`);
|
|
85
111
|
return;
|
|
86
112
|
}
|
|
87
|
-
this.enqueue(() => runWorkflowOnce(wf, {
|
|
113
|
+
this.enqueue(() => runWorkflowOnce(wf, {
|
|
114
|
+
registry: this.registry,
|
|
115
|
+
services: this.services,
|
|
116
|
+
triggerNodeId: node.id,
|
|
117
|
+
observer: this.observer,
|
|
118
|
+
})
|
|
88
119
|
.then(() => { })
|
|
89
120
|
// A failed scheduled run must not crash the scheduler (unhandled
|
|
90
121
|
// rejection). The failure is already reported to the observer; log
|
|
91
122
|
// and carry on so the next trigger still fires.
|
|
92
123
|
.catch((err) => this.services.logger?.error?.("scheduled run failed", {
|
|
93
|
-
workflow: wf.name,
|
|
124
|
+
workflow: wf.name,
|
|
125
|
+
error: err instanceof Error ? err.message : String(err),
|
|
94
126
|
})));
|
|
95
127
|
}));
|
|
96
128
|
}
|
|
@@ -104,10 +136,19 @@ export class Scheduler {
|
|
|
104
136
|
while (this.active < 4 && this.pending.length > 0) {
|
|
105
137
|
const run = this.pending.shift();
|
|
106
138
|
this.active++;
|
|
107
|
-
run()
|
|
108
|
-
.
|
|
139
|
+
run()
|
|
140
|
+
.catch((e) => this.services.logger.error("scheduled run failed", {
|
|
141
|
+
error: String(e),
|
|
142
|
+
}))
|
|
143
|
+
.finally(() => {
|
|
144
|
+
this.active--;
|
|
145
|
+
this.drain();
|
|
146
|
+
});
|
|
109
147
|
}
|
|
110
148
|
}
|
|
111
|
-
stop() {
|
|
112
|
-
|
|
149
|
+
stop() {
|
|
150
|
+
for (const c of this.crons)
|
|
151
|
+
c.stop();
|
|
152
|
+
this.crons = [];
|
|
153
|
+
}
|
|
113
154
|
}
|
package/dist/schema.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
3
|
const nodeType = z.enum([
|
|
4
|
-
"trigger.cron",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"task.
|
|
4
|
+
"trigger.cron",
|
|
5
|
+
"trigger.webhook",
|
|
6
|
+
"trigger.command",
|
|
7
|
+
"task.http",
|
|
8
|
+
"task.transform",
|
|
9
|
+
"task.branch",
|
|
10
|
+
"task.run-workflow",
|
|
11
|
+
"task.foreach",
|
|
12
|
+
"task.cache-unseen",
|
|
13
|
+
"task.cache-mark",
|
|
8
14
|
"agent.run",
|
|
9
15
|
]);
|
|
10
|
-
const retry = z.object({
|
|
16
|
+
const retry = z.object({
|
|
17
|
+
maxAttempts: z.number().int().min(1),
|
|
18
|
+
backoffMs: z.number().int().min(0),
|
|
19
|
+
});
|
|
11
20
|
const NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
12
21
|
const node = z.object({
|
|
13
|
-
id: z
|
|
22
|
+
id: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.superRefine((v, ctx) => {
|
|
14
26
|
if (!NODE_ID_RE.test(v)) {
|
|
15
|
-
ctx.addIssue({
|
|
27
|
+
ctx.addIssue({
|
|
28
|
+
code: "custom",
|
|
29
|
+
message: `node id "${v}" is not a valid identifier (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`,
|
|
30
|
+
});
|
|
16
31
|
}
|
|
17
32
|
}),
|
|
18
33
|
type: nodeType,
|
|
@@ -20,31 +35,51 @@ const node = z.object({
|
|
|
20
35
|
retry: retry.optional(),
|
|
21
36
|
onError: z.enum(["fail", "continue"]).optional(),
|
|
22
37
|
});
|
|
23
|
-
const edge = z.object({
|
|
24
|
-
|
|
38
|
+
const edge = z.object({
|
|
39
|
+
from: z.string().min(1),
|
|
40
|
+
to: z.string().min(1),
|
|
41
|
+
when: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
const workflow = z
|
|
44
|
+
.object({
|
|
25
45
|
name: z.string().min(1),
|
|
26
46
|
title: z.string().min(1).optional(),
|
|
47
|
+
// Optional sidebar grouping. A `/` denotes sub-nesting (e.g. "Sentry/Bug pipeline").
|
|
48
|
+
group: z.string().min(1).optional(),
|
|
27
49
|
nodes: z.array(node).min(1),
|
|
28
50
|
edges: z.array(edge).default([]),
|
|
29
|
-
})
|
|
30
|
-
|
|
51
|
+
})
|
|
52
|
+
.superRefine((wf, ctx) => {
|
|
53
|
+
const ids = new Set(wf.nodes.map((n) => n.id));
|
|
31
54
|
for (const e of wf.edges) {
|
|
32
55
|
if (!ids.has(e.from))
|
|
33
|
-
ctx.addIssue({
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: "custom",
|
|
58
|
+
message: `edge.from references missing node: ${e.from}`,
|
|
59
|
+
});
|
|
34
60
|
if (!ids.has(e.to))
|
|
35
|
-
ctx.addIssue({
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: "custom",
|
|
63
|
+
message: `edge.to references missing node: ${e.to}`,
|
|
64
|
+
});
|
|
36
65
|
}
|
|
37
66
|
for (const n of wf.nodes) {
|
|
38
67
|
if (n.type === "trigger.cron") {
|
|
39
68
|
const cron = n.config.cron;
|
|
40
69
|
if (typeof cron !== "string" || cron.trim() === "") {
|
|
41
|
-
ctx.addIssue({
|
|
70
|
+
ctx.addIssue({
|
|
71
|
+
code: "custom",
|
|
72
|
+
message: `node "${n.id}" (trigger.cron) requires a non-empty config.cron string`,
|
|
73
|
+
});
|
|
42
74
|
}
|
|
43
75
|
}
|
|
44
76
|
if (n.type === "task.run-workflow") {
|
|
45
77
|
const workflow = n.config.workflow;
|
|
46
78
|
if (typeof workflow !== "string" || workflow.trim() === "") {
|
|
47
|
-
ctx.addIssue({
|
|
79
|
+
ctx.addIssue({
|
|
80
|
+
code: "custom",
|
|
81
|
+
message: `node "${n.id}" (task.run-workflow) requires a non-empty config.workflow string`,
|
|
82
|
+
});
|
|
48
83
|
}
|
|
49
84
|
}
|
|
50
85
|
if (n.type === "task.foreach") {
|
|
@@ -52,25 +87,40 @@ const workflow = z.object({
|
|
|
52
87
|
const items = cfg.items;
|
|
53
88
|
const wfName = cfg.workflow;
|
|
54
89
|
if (typeof items !== "string" || items.trim() === "") {
|
|
55
|
-
ctx.addIssue({
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: "custom",
|
|
92
|
+
message: `node "${n.id}" (task.foreach) requires a non-empty config.items string`,
|
|
93
|
+
});
|
|
56
94
|
}
|
|
57
95
|
if (typeof wfName !== "string" || wfName.trim() === "") {
|
|
58
|
-
ctx.addIssue({
|
|
96
|
+
ctx.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
message: `node "${n.id}" (task.foreach) requires a non-empty config.workflow string`,
|
|
99
|
+
});
|
|
59
100
|
}
|
|
60
101
|
}
|
|
61
102
|
if (n.type === "task.cache-unseen") {
|
|
62
103
|
const cfg = n.config;
|
|
63
104
|
if (typeof cfg.items !== "string" || cfg.items.trim() === "") {
|
|
64
|
-
ctx.addIssue({
|
|
105
|
+
ctx.addIssue({
|
|
106
|
+
code: "custom",
|
|
107
|
+
message: `node "${n.id}" (task.cache-unseen) requires a non-empty config.items string`,
|
|
108
|
+
});
|
|
65
109
|
}
|
|
66
110
|
if (typeof cfg.key !== "string" || cfg.key.trim() === "") {
|
|
67
|
-
ctx.addIssue({
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: "custom",
|
|
113
|
+
message: `node "${n.id}" (task.cache-unseen) requires a non-empty config.key string`,
|
|
114
|
+
});
|
|
68
115
|
}
|
|
69
116
|
}
|
|
70
117
|
if (n.type === "task.cache-mark") {
|
|
71
118
|
const cfg = n.config;
|
|
72
119
|
if (typeof cfg.keys !== "string" || cfg.keys.trim() === "") {
|
|
73
|
-
ctx.addIssue({
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: "custom",
|
|
122
|
+
message: `node "${n.id}" (task.cache-mark) requires a non-empty config.keys string`,
|
|
123
|
+
});
|
|
74
124
|
}
|
|
75
125
|
}
|
|
76
126
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentRunner, McpServerRef, AgentActivity, Capabilities, PlanUsage, PlanWindow } from "@agentic-surfaces/agent";
|
|
2
2
|
import type { AgentDefinition } from "./agents.js";
|
|
3
|
-
export type { AgentRunner, McpServerRef, AgentActivity, Capabilities, PlanUsage, PlanWindow };
|
|
3
|
+
export type { AgentRunner, McpServerRef, AgentActivity, Capabilities, PlanUsage, PlanWindow, };
|
|
4
4
|
export type NodeType = "trigger.cron" | "trigger.webhook" | "trigger.command" | "task.http" | "task.transform" | "task.branch" | "task.run-workflow" | "task.foreach" | "task.cache-unseen" | "task.cache-mark" | "agent.run";
|
|
5
5
|
export interface RetryPolicy {
|
|
6
6
|
maxAttempts: number;
|
|
@@ -22,6 +22,12 @@ export interface Workflow {
|
|
|
22
22
|
name: string;
|
|
23
23
|
/** Human-readable display name for the UI (falls back to `name` when omitted). */
|
|
24
24
|
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional sidebar grouping. A `/` denotes sub-nesting (e.g. "Sentry/Bug pipeline").
|
|
27
|
+
* When omitted, the CLI falls back to the workflow file's subdirectory path relative
|
|
28
|
+
* to the workflows dir; a workflow at the root with no `group` renders ungrouped.
|
|
29
|
+
*/
|
|
30
|
+
group?: string;
|
|
25
31
|
nodes: WorkflowNode[];
|
|
26
32
|
edges: WorkflowEdge[];
|
|
27
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentic-surfaces/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"jsonata": "^2.2.1",
|
|
25
25
|
"yaml": "^2.9.0",
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@agentic-surfaces/agent": "0.1.
|
|
27
|
+
"@agentic-surfaces/agent": "0.1.30"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/better-sqlite3": "^7.6.13",
|