@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.
@@ -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
- ?? workflow.nodes.find(n => n.type.startsWith("trigger."))?.id;
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({ workflow, triggerNodeId, registry, services: opts.services, payload: opts.payload, observer: opts.observer });
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, { registry, services: subServices, observer, payload })
53
- .finally(() => { depth--; });
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) { this.workflows.push(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, { registry: this.registry, services: this.services, triggerNodeId: node.id, observer: this.observer })
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, error: err instanceof Error ? err.message : String(err),
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().catch(e => this.services.logger.error("scheduled run failed", { error: String(e) }))
108
- .finally(() => { this.active--; this.drain(); });
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() { for (const c of this.crons)
112
- c.stop(); this.crons = []; }
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", "trigger.webhook", "trigger.command",
5
- "task.http", "task.transform", "task.branch",
6
- "task.run-workflow", "task.foreach",
7
- "task.cache-unseen", "task.cache-mark",
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({ maxAttempts: z.number().int().min(1), backoffMs: z.number().int().min(0) });
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.string().min(1).superRefine((v, ctx) => {
22
+ id: z
23
+ .string()
24
+ .min(1)
25
+ .superRefine((v, ctx) => {
14
26
  if (!NODE_ID_RE.test(v)) {
15
- ctx.addIssue({ code: "custom", message: `node id "${v}" is not a valid identifier (must match /^[A-Za-z_][A-Za-z0-9_]*$/)` });
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({ from: z.string().min(1), to: z.string().min(1), when: z.string().optional() });
24
- const workflow = z.object({
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
- }).superRefine((wf, ctx) => {
30
- const ids = new Set(wf.nodes.map(n => n.id));
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({ code: "custom", message: `edge.from references missing node: ${e.from}` });
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({ code: "custom", message: `edge.to references missing node: ${e.to}` });
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({ code: "custom", message: `node "${n.id}" (trigger.cron) requires a non-empty config.cron string` });
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({ code: "custom", message: `node "${n.id}" (task.run-workflow) requires a non-empty config.workflow string` });
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({ code: "custom", message: `node "${n.id}" (task.foreach) requires a non-empty config.items string` });
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({ code: "custom", message: `node "${n.id}" (task.foreach) requires a non-empty config.workflow string` });
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({ code: "custom", message: `node "${n.id}" (task.cache-unseen) requires a non-empty config.items string` });
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({ code: "custom", message: `node "${n.id}" (task.cache-unseen) requires a non-empty config.key string` });
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({ code: "custom", message: `node "${n.id}" (task.cache-mark) requires a non-empty config.keys string` });
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.29",
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.29"
27
+ "@agentic-surfaces/agent": "0.1.30"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/better-sqlite3": "^7.6.13",