@agentic-surfaces/server 0.1.29 → 0.1.31
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/editor/assets/{index-HQKltIcN.css → index-D-uLRZZ-.css} +1 -1
- package/dist/editor/assets/index-DeFJLzQ_.js +298 -0
- package/dist/editor/index.html +2 -2
- package/dist/http.d.ts +7 -0
- package/dist/http.js +94 -14
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +7 -2
- package/dist/streaming-observer.d.ts +10 -1
- package/dist/streaming-observer.js +47 -5
- package/package.json +2 -2
- package/dist/editor/assets/index-kTJFLfkQ.js +0 -298
package/dist/editor/index.html
CHANGED
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DeFJLzQ_.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
|
@@ -56,6 +56,13 @@ export interface ServerOptions {
|
|
|
56
56
|
};
|
|
57
57
|
/** Best-effort claude.ai plan usage provider, surfaced (cached) at GET /api/usage. */
|
|
58
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[];
|
|
59
66
|
}
|
|
60
67
|
/** Build an http.Server that serves the workflow-engine API and optional static files. */
|
|
61
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,15 @@ 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,
|
|
30
|
-
|
|
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
|
+
}
|
|
31
46
|
// Cache plan usage briefly — the experimental SDK call spins up a session, so don't hit it per request.
|
|
32
47
|
let usageCache = null;
|
|
33
48
|
const PLAN_USAGE_TTL_MS = 5 * 60_000;
|
|
@@ -92,7 +107,9 @@ export function createServer(opts) {
|
|
|
92
107
|
json: "application/json",
|
|
93
108
|
woff2: "font/woff2",
|
|
94
109
|
};
|
|
95
|
-
res.writeHead(200, {
|
|
110
|
+
res.writeHead(200, {
|
|
111
|
+
"Content-Type": mime[ext] ?? "application/octet-stream",
|
|
112
|
+
});
|
|
96
113
|
res.end(buf);
|
|
97
114
|
return true;
|
|
98
115
|
}
|
|
@@ -105,17 +122,40 @@ export function createServer(opts) {
|
|
|
105
122
|
const pathname = url.pathname;
|
|
106
123
|
// CORS preflight
|
|
107
124
|
if (req.method === "OPTIONS") {
|
|
108
|
-
res.writeHead(204, {
|
|
125
|
+
res.writeHead(204, {
|
|
126
|
+
"Access-Control-Allow-Origin": "*",
|
|
127
|
+
"Access-Control-Allow-Headers": "*",
|
|
128
|
+
});
|
|
109
129
|
res.end();
|
|
110
130
|
return;
|
|
111
131
|
}
|
|
112
132
|
// ── API routes ──────────────────────────────────────────────────────────
|
|
113
133
|
if (pathname === "/api/workflows") {
|
|
134
|
+
// Sub-flow detection: scan ALL workflows' `task.foreach` nodes for the sub-workflow they
|
|
135
|
+
// fan out into (`config.workflow`), and record which parent(s) invoke each one. A workflow
|
|
136
|
+
// with a non-empty `invokedBy` is a "sub-flow" — it only runs per-item from a parent and
|
|
137
|
+
// needs that parent's payload, so the dashboard renders it nested + non-runnable.
|
|
138
|
+
const invokedBy = new Map();
|
|
139
|
+
for (const w of workflows) {
|
|
140
|
+
for (const n of w.nodes) {
|
|
141
|
+
if (n.type !== "task.foreach")
|
|
142
|
+
continue;
|
|
143
|
+
const target = n.config.workflow;
|
|
144
|
+
if (typeof target !== "string" || target.trim() === "")
|
|
145
|
+
continue;
|
|
146
|
+
const parents = invokedBy.get(target) ?? [];
|
|
147
|
+
if (!parents.includes(w.name))
|
|
148
|
+
parents.push(w.name);
|
|
149
|
+
invokedBy.set(target, parents);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
114
152
|
json(res, workflows.map((w) => {
|
|
115
153
|
const triggerType = w.nodes.find((n) => n.type.startsWith("trigger."))?.type ?? null;
|
|
116
154
|
return {
|
|
117
155
|
name: w.name,
|
|
118
156
|
title: w.title ?? w.name,
|
|
157
|
+
// Sidebar grouping (explicit `group:` field or the file's subdir path; undefined = ungrouped).
|
|
158
|
+
group: w.group,
|
|
119
159
|
nodeCount: w.nodes.length,
|
|
120
160
|
triggerType,
|
|
121
161
|
// Full graph so the editor can render each workflow without a second fetch.
|
|
@@ -125,10 +165,31 @@ export function createServer(opts) {
|
|
|
125
165
|
// needs a payload (the editor prompts for one); entry workflows run with no payload.
|
|
126
166
|
runnable: Boolean(onRun),
|
|
127
167
|
needsPayload: triggerType === "trigger.command",
|
|
168
|
+
// Names of workflows whose `task.foreach` fans out into this one (empty = entry/top-level).
|
|
169
|
+
invokedBy: invokedBy.get(w.name) ?? [],
|
|
128
170
|
};
|
|
129
171
|
}));
|
|
130
172
|
return;
|
|
131
173
|
}
|
|
174
|
+
// Hot-reload the workflow set (re-scan dirs, rebuild map, re-register the scheduler) without a
|
|
175
|
+
// restart. The CLI supplies onReload; it returns the freshly-loaded workflows, which we swap into
|
|
176
|
+
// the live set and broadcast to connected dashboards so they re-fetch.
|
|
177
|
+
if (pathname === "/api/reload" && req.method === "POST") {
|
|
178
|
+
if (!onReload) {
|
|
179
|
+
json(res, { error: "reload is not enabled on this server" }, 405);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const next = await onReload();
|
|
184
|
+
setWorkflows(next);
|
|
185
|
+
observer.notifyWorkflowsChanged();
|
|
186
|
+
json(res, { reloaded: next.length, workflows: next.map((w) => w.name) }, 200);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
132
193
|
// Trigger a run from the UI. Fire-and-forget: progress streams via /api/events.
|
|
133
194
|
if (pathname === "/api/run" && req.method === "POST") {
|
|
134
195
|
if (!onRun) {
|
|
@@ -136,7 +197,9 @@ export function createServer(opts) {
|
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
138
199
|
let body = "";
|
|
139
|
-
req.on("data", (c) => {
|
|
200
|
+
req.on("data", (c) => {
|
|
201
|
+
body += c;
|
|
202
|
+
});
|
|
140
203
|
req.on("end", () => {
|
|
141
204
|
let parsed;
|
|
142
205
|
try {
|
|
@@ -153,18 +216,25 @@ export function createServer(opts) {
|
|
|
153
216
|
}
|
|
154
217
|
// Don't await — the run streams over SSE; a thrown error is reported
|
|
155
218
|
// there (and logged by the runner), so the server keeps serving.
|
|
156
|
-
Promise.resolve(onRun(name, parsed.payload)).catch(() => {
|
|
219
|
+
Promise.resolve(onRun(name, parsed.payload)).catch(() => {
|
|
220
|
+
/* surfaced via observer */
|
|
221
|
+
});
|
|
157
222
|
json(res, { started: name }, 202);
|
|
158
223
|
});
|
|
159
224
|
return;
|
|
160
225
|
}
|
|
161
226
|
// Platform-wide play/pause. GET reports state; POST /api/pause|resume toggles the scheduler.
|
|
162
|
-
if (pathname === "/api/state" &&
|
|
163
|
-
|
|
227
|
+
if (pathname === "/api/state" &&
|
|
228
|
+
(req.method === "GET" || req.method === undefined)) {
|
|
229
|
+
json(res, {
|
|
230
|
+
paused: pause ? pause.isPaused() : false,
|
|
231
|
+
controllable: Boolean(pause),
|
|
232
|
+
});
|
|
164
233
|
return;
|
|
165
234
|
}
|
|
166
235
|
// Best-effort claude.ai plan usage (session + weekly windows) for the spend/balance box.
|
|
167
|
-
if (pathname === "/api/usage" &&
|
|
236
|
+
if (pathname === "/api/usage" &&
|
|
237
|
+
(req.method === "GET" || req.method === undefined)) {
|
|
168
238
|
const data = await getCachedPlanUsage();
|
|
169
239
|
json(res, data ?? { available: false });
|
|
170
240
|
return;
|
|
@@ -188,7 +258,8 @@ export function createServer(opts) {
|
|
|
188
258
|
return;
|
|
189
259
|
}
|
|
190
260
|
// Inspect / clear the engine seen-cache (debugging dedup state).
|
|
191
|
-
if (pathname === "/api/cache" &&
|
|
261
|
+
if (pathname === "/api/cache" &&
|
|
262
|
+
(req.method === "GET" || req.method === undefined)) {
|
|
192
263
|
json(res, cache ? cache.entries() : { seen: [], cursors: [] });
|
|
193
264
|
return;
|
|
194
265
|
}
|
|
@@ -198,7 +269,9 @@ export function createServer(opts) {
|
|
|
198
269
|
return;
|
|
199
270
|
}
|
|
200
271
|
let body = "";
|
|
201
|
-
req.on("data", (c) => {
|
|
272
|
+
req.on("data", (c) => {
|
|
273
|
+
body += c;
|
|
274
|
+
});
|
|
202
275
|
req.on("end", () => {
|
|
203
276
|
let key;
|
|
204
277
|
try {
|
|
@@ -222,10 +295,17 @@ export function createServer(opts) {
|
|
|
222
295
|
return;
|
|
223
296
|
}
|
|
224
297
|
if (pathname === "/api/agents") {
|
|
225
|
-
// Metadata
|
|
298
|
+
// Metadata + parsed frontmatter + the instructions body (capped), so the
|
|
299
|
+
// node-detail view can show an agent node's full definition file.
|
|
226
300
|
json(res, agents.map((a) => ({
|
|
227
|
-
name: a.name,
|
|
228
|
-
|
|
301
|
+
name: a.name,
|
|
302
|
+
model: a.model,
|
|
303
|
+
effort: a.effort,
|
|
304
|
+
profile: a.profile ?? null,
|
|
305
|
+
commands: a.commands ?? null,
|
|
306
|
+
fallback: a.fallback ?? null,
|
|
307
|
+
mcpServers: a.mcpServers ?? null,
|
|
308
|
+
instructions: capBody(a.instructions ?? ""),
|
|
229
309
|
})));
|
|
230
310
|
return;
|
|
231
311
|
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface ServeOptions {
|
|
|
46
46
|
};
|
|
47
47
|
/** Best-effort claude.ai plan usage provider, surfaced (cached) at GET /api/usage. */
|
|
48
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[];
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Locate the built editor assets to serve. Checks, in order:
|
package/dist/serve.js
CHANGED
|
@@ -31,7 +31,10 @@ 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,
|
|
34
|
+
observer,
|
|
35
|
+
port,
|
|
36
|
+
host,
|
|
37
|
+
staticDir,
|
|
35
38
|
workflows: opts.workflows,
|
|
36
39
|
onRun: opts.onRun,
|
|
37
40
|
config: opts.config,
|
|
@@ -39,6 +42,7 @@ export function serve(opts = {}) {
|
|
|
39
42
|
cache: opts.cache,
|
|
40
43
|
pause: opts.pause,
|
|
41
44
|
planUsage: opts.planUsage,
|
|
45
|
+
onReload: opts.onReload,
|
|
42
46
|
});
|
|
43
47
|
server.listen(port, host, () => {
|
|
44
48
|
console.log(`[flow-server] listening on http://${host}:${port}`);
|
|
@@ -53,7 +57,8 @@ export function serve(opts = {}) {
|
|
|
53
57
|
// Only run when executed directly as a script (not when imported as a module).
|
|
54
58
|
const isMain = typeof process !== "undefined" &&
|
|
55
59
|
process.argv[1] != null &&
|
|
56
|
-
(process.argv[1].endsWith("serve.js") ||
|
|
60
|
+
(process.argv[1].endsWith("serve.js") ||
|
|
61
|
+
process.argv[1].endsWith("flow-server"));
|
|
57
62
|
if (isMain) {
|
|
58
63
|
const port = parseInt(process.env["PORT"] ?? "4000", 10);
|
|
59
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 &&
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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.
|
|
3
|
+
"version": "0.1.31",
|
|
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.
|
|
25
|
+
"@agentic-surfaces/core": "0.1.31"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|