@agentic-surfaces/server 0.1.28 → 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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>agentic-surfaces — workflow editor</title>
7
- <script type="module" crossorigin src="/assets/index-C7_sNeWL.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CQrXDTe4.css">
7
+ <script type="module" crossorigin src="/assets/index-Vur7bXRy.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D-uLRZZ-.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "node:http";
2
- import type { Workflow, AgentDefinition } from "@agentic-surfaces/core";
2
+ import type { Workflow, AgentDefinition, PlanUsage } from "@agentic-surfaces/core";
3
3
  import type { StreamingObserver } from "./streaming-observer.js";
4
4
  /** Minimal run record kept in-memory for the /api/runs endpoint. */
5
5
  export interface RunRecord {
@@ -54,6 +54,15 @@ export interface ServerOptions {
54
54
  pause(): void;
55
55
  resume(): void;
56
56
  };
57
+ /** Best-effort claude.ai plan usage provider, surfaced (cached) at GET /api/usage. */
58
+ planUsage?: () => Promise<PlanUsage | null>;
59
+ /**
60
+ * Hot-reload hook (POST /api/reload). Re-scans the workflows + agents dirs and project config,
61
+ * rebuilds the in-memory workflow map, and re-registers the scheduler in place. Returns the new
62
+ * workflow set so the server can serve it from /api/workflows and /api/run without a restart.
63
+ * When omitted, POST /api/reload is 405 (reload not available in this mode, e.g. run-once).
64
+ */
65
+ onReload?: () => Promise<Workflow[]> | Workflow[];
57
66
  }
58
67
  /** Build an http.Server that serves the workflow-engine API and optional static files. */
59
68
  export declare function createServer(opts: ServerOptions): http.Server;
package/dist/http.js CHANGED
@@ -13,6 +13,14 @@ function json(res, data, status = 200) {
13
13
  function notFound(res) {
14
14
  json(res, { error: "not found" }, 404);
15
15
  }
16
+ /** Max characters of an agent's instructions body we put on the wire — bounded so a long
17
+ * definition can't bloat the payload (mirrors streaming-observer's OUTPUT_PREVIEW_CAP). */
18
+ const AGENT_BODY_CAP = 16_000;
19
+ function capBody(s) {
20
+ return s.length > AGENT_BODY_CAP
21
+ ? `${s.slice(0, AGENT_BODY_CAP)}\n…(${s.length - AGENT_BODY_CAP} more chars truncated)`
22
+ : s;
23
+ }
16
24
  function sseHeaders(res) {
17
25
  res.writeHead(200, {
18
26
  "Content-Type": "text/event-stream",
@@ -26,8 +34,33 @@ function writeEvent(res, event) {
26
34
  }
27
35
  /** Build an http.Server that serves the workflow-engine API and optional static files. */
28
36
  export function createServer(opts) {
29
- const { observer, workflows = [], staticDir, onRun, config, agents = [], cache, pause } = opts;
30
- const workflowByName = new Map(workflows.map((w) => [w.name, w]));
37
+ const { observer, staticDir, onRun, config, agents = [], cache, pause, planUsage, onReload, } = opts;
38
+ // Live workflow set — mutable so POST /api/reload (+ the fs.watch reload path) can swap it in
39
+ // place without a server restart. /api/workflows, /api/run, and the graph view all read from here.
40
+ let workflows = opts.workflows ?? [];
41
+ let workflowByName = new Map(workflows.map((w) => [w.name, w]));
42
+ function setWorkflows(next) {
43
+ workflows = next;
44
+ workflowByName = new Map(next.map((w) => [w.name, w]));
45
+ }
46
+ // Cache plan usage briefly — the experimental SDK call spins up a session, so don't hit it per request.
47
+ let usageCache = null;
48
+ const PLAN_USAGE_TTL_MS = 5 * 60_000;
49
+ async function getCachedPlanUsage() {
50
+ if (!planUsage)
51
+ return null;
52
+ if (usageCache && Date.now() - usageCache.at < PLAN_USAGE_TTL_MS)
53
+ return usageCache.data;
54
+ let data = null;
55
+ try {
56
+ data = await planUsage();
57
+ }
58
+ catch {
59
+ data = null;
60
+ }
61
+ usageCache = { data, at: Date.now() };
62
+ return data;
63
+ }
31
64
  // In-memory run registry — populated by listening to the observer.
32
65
  const runs = new Map();
33
66
  let runCounter = 0;
@@ -74,7 +107,9 @@ export function createServer(opts) {
74
107
  json: "application/json",
75
108
  woff2: "font/woff2",
76
109
  };
77
- res.writeHead(200, { "Content-Type": mime[ext] ?? "application/octet-stream" });
110
+ res.writeHead(200, {
111
+ "Content-Type": mime[ext] ?? "application/octet-stream",
112
+ });
78
113
  res.end(buf);
79
114
  return true;
80
115
  }
@@ -87,7 +122,10 @@ export function createServer(opts) {
87
122
  const pathname = url.pathname;
88
123
  // CORS preflight
89
124
  if (req.method === "OPTIONS") {
90
- res.writeHead(204, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*" });
125
+ res.writeHead(204, {
126
+ "Access-Control-Allow-Origin": "*",
127
+ "Access-Control-Allow-Headers": "*",
128
+ });
91
129
  res.end();
92
130
  return;
93
131
  }
@@ -98,6 +136,8 @@ export function createServer(opts) {
98
136
  return {
99
137
  name: w.name,
100
138
  title: w.title ?? w.name,
139
+ // Sidebar grouping (explicit `group:` field or the file's subdir path; undefined = ungrouped).
140
+ group: w.group,
101
141
  nodeCount: w.nodes.length,
102
142
  triggerType,
103
143
  // Full graph so the editor can render each workflow without a second fetch.
@@ -111,6 +151,25 @@ export function createServer(opts) {
111
151
  }));
112
152
  return;
113
153
  }
154
+ // Hot-reload the workflow set (re-scan dirs, rebuild map, re-register the scheduler) without a
155
+ // restart. The CLI supplies onReload; it returns the freshly-loaded workflows, which we swap into
156
+ // the live set and broadcast to connected dashboards so they re-fetch.
157
+ if (pathname === "/api/reload" && req.method === "POST") {
158
+ if (!onReload) {
159
+ json(res, { error: "reload is not enabled on this server" }, 405);
160
+ return;
161
+ }
162
+ try {
163
+ const next = await onReload();
164
+ setWorkflows(next);
165
+ observer.notifyWorkflowsChanged();
166
+ json(res, { reloaded: next.length, workflows: next.map((w) => w.name) }, 200);
167
+ }
168
+ catch (err) {
169
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
170
+ }
171
+ return;
172
+ }
114
173
  // Trigger a run from the UI. Fire-and-forget: progress streams via /api/events.
115
174
  if (pathname === "/api/run" && req.method === "POST") {
116
175
  if (!onRun) {
@@ -118,7 +177,9 @@ export function createServer(opts) {
118
177
  return;
119
178
  }
120
179
  let body = "";
121
- req.on("data", (c) => { body += c; });
180
+ req.on("data", (c) => {
181
+ body += c;
182
+ });
122
183
  req.on("end", () => {
123
184
  let parsed;
124
185
  try {
@@ -135,14 +196,27 @@ export function createServer(opts) {
135
196
  }
136
197
  // Don't await — the run streams over SSE; a thrown error is reported
137
198
  // there (and logged by the runner), so the server keeps serving.
138
- Promise.resolve(onRun(name, parsed.payload)).catch(() => { });
199
+ Promise.resolve(onRun(name, parsed.payload)).catch(() => {
200
+ /* surfaced via observer */
201
+ });
139
202
  json(res, { started: name }, 202);
140
203
  });
141
204
  return;
142
205
  }
143
206
  // Platform-wide play/pause. GET reports state; POST /api/pause|resume toggles the scheduler.
144
- if (pathname === "/api/state" && (req.method === "GET" || req.method === undefined)) {
145
- json(res, { paused: pause ? pause.isPaused() : false, controllable: Boolean(pause) });
207
+ if (pathname === "/api/state" &&
208
+ (req.method === "GET" || req.method === undefined)) {
209
+ json(res, {
210
+ paused: pause ? pause.isPaused() : false,
211
+ controllable: Boolean(pause),
212
+ });
213
+ return;
214
+ }
215
+ // Best-effort claude.ai plan usage (session + weekly windows) for the spend/balance box.
216
+ if (pathname === "/api/usage" &&
217
+ (req.method === "GET" || req.method === undefined)) {
218
+ const data = await getCachedPlanUsage();
219
+ json(res, data ?? { available: false });
146
220
  return;
147
221
  }
148
222
  if (pathname === "/api/pause" && req.method === "POST") {
@@ -164,7 +238,8 @@ export function createServer(opts) {
164
238
  return;
165
239
  }
166
240
  // Inspect / clear the engine seen-cache (debugging dedup state).
167
- if (pathname === "/api/cache" && (req.method === "GET" || req.method === undefined)) {
241
+ if (pathname === "/api/cache" &&
242
+ (req.method === "GET" || req.method === undefined)) {
168
243
  json(res, cache ? cache.entries() : { seen: [], cursors: [] });
169
244
  return;
170
245
  }
@@ -174,7 +249,9 @@ export function createServer(opts) {
174
249
  return;
175
250
  }
176
251
  let body = "";
177
- req.on("data", (c) => { body += c; });
252
+ req.on("data", (c) => {
253
+ body += c;
254
+ });
178
255
  req.on("end", () => {
179
256
  let key;
180
257
  try {
@@ -198,10 +275,17 @@ export function createServer(opts) {
198
275
  return;
199
276
  }
200
277
  if (pathname === "/api/agents") {
201
- // Metadata only not the full instructions body.
278
+ // Metadata + parsed frontmatter + the instructions body (capped), so the
279
+ // node-detail view can show an agent node's full definition file.
202
280
  json(res, agents.map((a) => ({
203
- name: a.name, model: a.model, effort: a.effort,
204
- commands: a.commands ?? null, fallback: a.fallback ?? null,
281
+ name: a.name,
282
+ model: a.model,
283
+ effort: a.effort,
284
+ profile: a.profile ?? null,
285
+ commands: a.commands ?? null,
286
+ fallback: a.fallback ?? null,
287
+ mcpServers: a.mcpServers ?? null,
288
+ instructions: capBody(a.instructions ?? ""),
205
289
  })));
206
290
  return;
207
291
  }
package/dist/serve.d.ts CHANGED
@@ -44,6 +44,10 @@ export interface ServeOptions {
44
44
  pause(): void;
45
45
  resume(): void;
46
46
  };
47
+ /** Best-effort claude.ai plan usage provider, surfaced (cached) at GET /api/usage. */
48
+ planUsage?: () => Promise<import("@agentic-surfaces/core").PlanUsage | null>;
49
+ /** Hot-reload hook (POST /api/reload): re-scan + rebuild the workflow set + scheduler in place. */
50
+ onReload?: () => Promise<Workflow[]> | Workflow[];
47
51
  }
48
52
  /**
49
53
  * Locate the built editor assets to serve. Checks, in order:
package/dist/serve.js CHANGED
@@ -31,13 +31,18 @@ export function serve(opts = {}) {
31
31
  const host = opts.host ?? "127.0.0.1";
32
32
  const staticDir = opts.staticDir ?? resolveEditorDir();
33
33
  const server = createServer({
34
- observer, port, host, staticDir,
34
+ observer,
35
+ port,
36
+ host,
37
+ staticDir,
35
38
  workflows: opts.workflows,
36
39
  onRun: opts.onRun,
37
40
  config: opts.config,
38
41
  agents: opts.agents,
39
42
  cache: opts.cache,
40
43
  pause: opts.pause,
44
+ planUsage: opts.planUsage,
45
+ onReload: opts.onReload,
41
46
  });
42
47
  server.listen(port, host, () => {
43
48
  console.log(`[flow-server] listening on http://${host}:${port}`);
@@ -52,7 +57,8 @@ export function serve(opts = {}) {
52
57
  // Only run when executed directly as a script (not when imported as a module).
53
58
  const isMain = typeof process !== "undefined" &&
54
59
  process.argv[1] != null &&
55
- (process.argv[1].endsWith("serve.js") || process.argv[1].endsWith("flow-server"));
60
+ (process.argv[1].endsWith("serve.js") ||
61
+ process.argv[1].endsWith("flow-server"));
56
62
  if (isMain) {
57
63
  const port = parseInt(process.env["PORT"] ?? "4000", 10);
58
64
  const staticDir = process.env["STATIC_DIR"];
@@ -1,7 +1,7 @@
1
1
  import type { RunObserver } from "@agentic-surfaces/core";
2
2
  /** A single SSE-ready event that the HTTP layer will broadcast. */
3
3
  export interface RunEvent {
4
- type: "run:start" | "node:start" | "node:finish" | "node:activity" | "run:finish";
4
+ type: "run:start" | "node:start" | "node:finish" | "node:activity" | "run:finish" | "workflows:changed";
5
5
  workflow: string;
6
6
  /** Which run this event belongs to (from the executor) — lets the UI separate concurrent runs. */
7
7
  runId?: string;
@@ -29,6 +29,15 @@ export declare class StreamingObserver implements RunObserver {
29
29
  private events;
30
30
  private listeners;
31
31
  private emit;
32
+ /** Deliver an event to every listener, isolating bad listeners. */
33
+ private fanout;
34
+ /**
35
+ * Notify connected clients that the loaded workflow set changed (after a reload),
36
+ * so the dashboard re-fetches /api/workflows + refreshes the open graph. This is a
37
+ * transient signal, NOT part of the run history, so it is fanned out live but NOT
38
+ * pushed onto the buffer (it must not replay on every SSE reconnect).
39
+ */
40
+ notifyWorkflowsChanged(): void;
32
41
  /** Subscribe to all future events. Returns an unsubscribe function. */
33
42
  subscribe(listener: (event: RunEvent) => void): () => void;
34
43
  /** Return a snapshot of all buffered events (safe to call at any time). */
@@ -24,7 +24,9 @@ function runLabel(payload) {
24
24
  function extractUsage(output) {
25
25
  if (output && typeof output === "object" && "usage" in output) {
26
26
  const u = output.usage;
27
- if (u && typeof u === "object" && typeof u.totalTokens === "number") {
27
+ if (u &&
28
+ typeof u === "object" &&
29
+ typeof u.totalTokens === "number") {
28
30
  return u;
29
31
  }
30
32
  }
@@ -57,6 +59,10 @@ export class StreamingObserver {
57
59
  listeners = [];
58
60
  emit(event) {
59
61
  this.events.push(event);
62
+ this.fanout(event);
63
+ }
64
+ /** Deliver an event to every listener, isolating bad listeners. */
65
+ fanout(event) {
60
66
  for (const listener of this.listeners) {
61
67
  try {
62
68
  listener(event);
@@ -66,6 +72,15 @@ export class StreamingObserver {
66
72
  }
67
73
  }
68
74
  }
75
+ /**
76
+ * Notify connected clients that the loaded workflow set changed (after a reload),
77
+ * so the dashboard re-fetches /api/workflows + refreshes the open graph. This is a
78
+ * transient signal, NOT part of the run history, so it is fanned out live but NOT
79
+ * pushed onto the buffer (it must not replay on every SSE reconnect).
80
+ */
81
+ notifyWorkflowsChanged() {
82
+ this.fanout({ type: "workflows:changed", workflow: "", ts: Date.now() });
83
+ }
69
84
  /** Subscribe to all future events. Returns an unsubscribe function. */
70
85
  subscribe(listener) {
71
86
  this.listeners.push(listener);
@@ -83,15 +98,34 @@ export class StreamingObserver {
83
98
  }
84
99
  // ── RunObserver interface ──────────────────────────────────────────────────
85
100
  onRunStart(workflow, payload, runId) {
86
- this.emit({ type: "run:start", workflow, runId, label: runLabel(payload), ts: Date.now() });
101
+ this.emit({
102
+ type: "run:start",
103
+ workflow,
104
+ runId,
105
+ label: runLabel(payload),
106
+ ts: Date.now(),
107
+ });
87
108
  }
88
109
  onNodeStart(nodeId, runId) {
89
110
  // workflow name is unknown at this call-site in the core interface;
90
111
  // emit a placeholder so the shape stays consistent.
91
- this.emit({ type: "node:start", workflow: "", runId, nodeId, ts: Date.now() });
112
+ this.emit({
113
+ type: "node:start",
114
+ workflow: "",
115
+ runId,
116
+ nodeId,
117
+ ts: Date.now(),
118
+ });
92
119
  }
93
120
  onNodeActivity(nodeId, activity, runId) {
94
- this.emit({ type: "node:activity", workflow: "", runId, nodeId, meta: activity, ts: Date.now() });
121
+ this.emit({
122
+ type: "node:activity",
123
+ workflow: "",
124
+ runId,
125
+ nodeId,
126
+ meta: activity,
127
+ ts: Date.now(),
128
+ });
95
129
  }
96
130
  onNodeFinish(nodeId, status, meta, runId) {
97
131
  // Normalize meta to { output?, error? }. `output` is the node's result (capped for the
@@ -103,7 +137,15 @@ export class StreamingObserver {
103
137
  output: "output" in m ? previewOutput(m.output) : undefined,
104
138
  usage: extractUsage(m.output),
105
139
  };
106
- this.emit({ type: "node:finish", workflow: "", runId, nodeId, status, meta: wireMeta, ts: Date.now() });
140
+ this.emit({
141
+ type: "node:finish",
142
+ workflow: "",
143
+ runId,
144
+ nodeId,
145
+ status,
146
+ meta: wireMeta,
147
+ ts: Date.now(),
148
+ });
107
149
  }
108
150
  onRunFinish(workflow, status, runId) {
109
151
  this.emit({ type: "run:finish", workflow, runId, status, ts: Date.now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-surfaces/server",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -22,7 +22,7 @@
22
22
  "README.md"
23
23
  ],
24
24
  "dependencies": {
25
- "@agentic-surfaces/core": "0.1.28"
25
+ "@agentic-surfaces/core": "0.1.30"
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",