@agentstorm/server 0.2.5 → 0.2.6

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,6 +4,8 @@ export interface PipelineServiceOptions {
4
4
  cwd: string;
5
5
  /** Resident daemon lifecycle adapter. */
6
6
  agentRuntime: AgentRuntime;
7
+ /** Override the daemon-owned global template root in isolated deployments/tests. */
8
+ templateHome?: string;
7
9
  }
8
10
  export interface PipelineServiceRequestOptions {
9
11
  method?: "GET" | "POST" | "PUT" | "DELETE";
@@ -9,7 +9,7 @@ import { ensurePipelineSessionPaths, pipelineSessionPaths, writeSessionJsonAtomi
9
9
  import { safeParseWorkflowSpec, parseTaskList, ApprovalCommentSchema, HumanInterventionActionSchema, PipelineStartInputSchema, ResourceRefSchema, WorkflowBundleSchema, } from "@agentstorm/protocol";
10
10
  import { buildPipelineStatusSummary } from "./pipeline-status.js";
11
11
  import { GlobalTemplateLibrary, } from "./template-library.js";
12
- import { assertInsidePackage, assertWorkflowDisplayName, latestWorkflowPackageVersion, listWorkflowPackageNames, readJsonFile, recordWorkflowPackageVersion, safePackageRelativePath, workflowPackageExists, workflowPackagePaths, workflowPromptRelativePath, writeJsonAtomic as writePackageJsonAtomic, } from "./workflow-package.js";
12
+ import { assertInsidePackage, assertWorkflowDisplayName, collectWorkflowPackageAssets, latestWorkflowPackageVersion, listWorkflowPackageNames, readJsonFile, recordWorkflowPackageVersion, safePackageRelativePath, workflowPackageExists, workflowPackagePaths, workflowPromptRelativePath, writeWorkflowPackageSnapshot, writeJsonAtomic as writePackageJsonAtomic, } from "./workflow-package.js";
13
13
  export function statusWithRunKind(status, events) {
14
14
  const created = events.find((event) => event.t === "run_created");
15
15
  if (!created)
@@ -35,7 +35,7 @@ export async function createPipelineService(opts) {
35
35
  // and existing Workflow routes usable in restricted test containers where
36
36
  // the optional global template directory is not writable yet.
37
37
  let templateLibrary = null;
38
- const getTemplateLibrary = () => (templateLibrary ??= new GlobalTemplateLibrary());
38
+ const getTemplateLibrary = () => (templateLibrary ??= new GlobalTemplateLibrary(opts.templateHome ? { home: opts.templateHome } : undefined));
39
39
  function getOrCreateKernel(workspaceId, rootPath) {
40
40
  const existing = kernels.get(workspaceId);
41
41
  if (existing)
@@ -181,9 +181,7 @@ export async function createPipelineService(opts) {
181
181
  router.post("/workspaces/:wid/register", (req, res) => {
182
182
  const rootPath = typeof req.body?.rootPath === "string" ? req.body.rootPath.trim() : "";
183
183
  if (!rootPath || !path.isAbsolute(rootPath) || !fs.existsSync(rootPath)) {
184
- res
185
- .status(400)
186
- .json({ error: "rootPath must be an existing absolute directory" });
184
+ res.status(400).json({ error: "rootPath must be an existing absolute directory" });
187
185
  return;
188
186
  }
189
187
  if (!fs.statSync(rootPath).isDirectory()) {
@@ -245,9 +243,7 @@ export async function createPipelineService(opts) {
245
243
  });
246
244
  router.post("/templates", (req, res) => {
247
245
  try {
248
- res
249
- .status(201)
250
- .json(getTemplateLibrary().create(req.body));
246
+ res.status(201).json(getTemplateLibrary().create(req.body));
251
247
  }
252
248
  catch (error) {
253
249
  res.status(400).json({
@@ -261,9 +257,7 @@ export async function createPipelineService(opts) {
261
257
  }
262
258
  catch (error) {
263
259
  const message = error instanceof Error ? error.message : String(error);
264
- res
265
- .status(message.includes("ETag mismatch") ? 409 : 400)
266
- .json({ error: message });
260
+ res.status(message.includes("ETag mismatch") ? 409 : 400).json({ error: message });
267
261
  }
268
262
  });
269
263
  router.delete("/templates/:id", (req, res) => {
@@ -326,6 +320,7 @@ export async function createPipelineService(opts) {
326
320
  ? readJsonFile(files.manifest)
327
321
  : {};
328
322
  const source = manifest.source;
323
+ const publication = manifest.publication;
329
324
  return [
330
325
  {
331
326
  id,
@@ -341,8 +336,7 @@ export async function createPipelineService(opts) {
341
336
  !Array.isArray(source) &&
342
337
  typeof source.templateId === "string"
343
338
  ? {
344
- templateId: source
345
- .templateId,
339
+ templateId: source.templateId,
346
340
  ...(Number.isSafeInteger(source.templateVersion)
347
341
  ? {
348
342
  templateVersion: Number(source.templateVersion),
@@ -350,6 +344,20 @@ export async function createPipelineService(opts) {
350
344
  : {}),
351
345
  }
352
346
  : {}),
347
+ ...(publication &&
348
+ typeof publication === "object" &&
349
+ !Array.isArray(publication) &&
350
+ typeof publication.templateId === "string"
351
+ ? {
352
+ publishedTemplateId: publication
353
+ .templateId,
354
+ ...(Number.isSafeInteger(publication.templateVersion)
355
+ ? {
356
+ publishedTemplateVersion: Number(publication.templateVersion),
357
+ }
358
+ : {}),
359
+ }
360
+ : {}),
353
361
  },
354
362
  ];
355
363
  }
@@ -422,9 +430,7 @@ export async function createPipelineService(opts) {
422
430
  res.status(404).json({ error: "workspace not found" });
423
431
  return;
424
432
  }
425
- const templateId = typeof req.body?.templateId === "string"
426
- ? req.body.templateId.trim()
427
- : "";
433
+ const templateId = typeof req.body?.templateId === "string" ? req.body.templateId.trim() : "";
428
434
  if (!templateId) {
429
435
  res.status(400).json({ error: "templateId is required" });
430
436
  return;
@@ -444,6 +450,121 @@ export async function createPipelineService(opts) {
444
450
  });
445
451
  }
446
452
  });
453
+ router.post("/workspaces/:wid/workflows/:id/copies", (req, res) => {
454
+ const wk = kernels.get(req.params.wid);
455
+ if (!wk) {
456
+ res.status(404).json({ error: "workspace not found" });
457
+ return;
458
+ }
459
+ if (!isSafeWorkflowId(req.params.id)) {
460
+ res.status(400).json({ error: "invalid workflow id" });
461
+ return;
462
+ }
463
+ try {
464
+ const source = readProjectWorkflowSnapshot(wk.rootPath, req.params.id);
465
+ const expectedEtag = req.header("if-match");
466
+ if (expectedEtag && expectedEtag !== etagOf(source.raw)) {
467
+ res.status(409).json({ error: "ETag mismatch" });
468
+ return;
469
+ }
470
+ const name = assertWorkflowDisplayName(String(req.body?.name ?? ""));
471
+ const now = nowBeijing();
472
+ const spec = { ...source.spec, name };
473
+ writeWorkflowPackageSnapshot({
474
+ workspaceRoot: wk.rootPath,
475
+ name,
476
+ spec,
477
+ layout: source.layout,
478
+ assets: source.assets,
479
+ manifest: {
480
+ schemaVersion: 1,
481
+ name,
482
+ description: source.manifest.description,
483
+ version: 1,
484
+ source: {
485
+ kind: "copy",
486
+ workflowName: req.params.id,
487
+ workflowVersion: latestWorkflowPackageVersion(source.files),
488
+ },
489
+ createdAt: now,
490
+ updatedAt: now,
491
+ },
492
+ });
493
+ const raw = JSON.stringify(spec, null, 2);
494
+ res.status(201).json({
495
+ id: name,
496
+ spec,
497
+ etag: etagOf(raw),
498
+ updatedAt: now,
499
+ version: 1,
500
+ description: source.manifest.description,
501
+ });
502
+ }
503
+ catch (error) {
504
+ const message = error instanceof Error ? error.message : String(error);
505
+ res.status(message.includes("已存在") ? 409 : 400).json({ error: message });
506
+ }
507
+ });
508
+ router.post("/workspaces/:wid/workflows/:id/global-template", (req, res) => {
509
+ const wk = kernels.get(req.params.wid);
510
+ if (!wk) {
511
+ res.status(404).json({ error: "workspace not found" });
512
+ return;
513
+ }
514
+ if (!isSafeWorkflowId(req.params.id)) {
515
+ res.status(400).json({ error: "invalid workflow id" });
516
+ return;
517
+ }
518
+ try {
519
+ const source = readProjectWorkflowSnapshot(wk.rootPath, req.params.id);
520
+ const expectedEtag = req.header("if-match");
521
+ if (expectedEtag && expectedEtag !== etagOf(source.raw)) {
522
+ res.status(409).json({ error: "ETag mismatch" });
523
+ return;
524
+ }
525
+ const mode = req.body?.mode === "replace" ? "replace" : "create";
526
+ const name = String(req.body?.name ?? "").trim();
527
+ if (!name)
528
+ throw new Error("模板名称不能为空");
529
+ const input = {
530
+ ...(typeof req.body?.templateId === "string" && req.body.templateId.trim()
531
+ ? { id: req.body.templateId.trim() }
532
+ : {}),
533
+ name,
534
+ description: typeof req.body?.description === "string"
535
+ ? req.body.description
536
+ : source.manifest.description,
537
+ spec: { ...source.spec, name },
538
+ layout: source.layout,
539
+ assets: source.assets,
540
+ };
541
+ if (mode === "replace" &&
542
+ (typeof req.body?.expectedTemplateEtag !== "string" ||
543
+ !req.body.expectedTemplateEtag.trim())) {
544
+ throw new Error("更新全局模板必须提供当前模板 ETag");
545
+ }
546
+ const template = mode === "replace"
547
+ ? getTemplateLibrary().update(String(req.body?.templateId ?? ""), input, typeof req.body?.expectedTemplateEtag === "string"
548
+ ? req.body.expectedTemplateEtag
549
+ : undefined)
550
+ : getTemplateLibrary().create(input);
551
+ writePackageJsonAtomic(source.files.manifest, {
552
+ ...source.manifest,
553
+ publication: { templateId: template.id, templateVersion: template.version },
554
+ updatedAt: nowBeijing(),
555
+ });
556
+ res.status(mode === "create" ? 201 : 200).json({
557
+ template,
558
+ publication: { templateId: template.id, templateVersion: template.version },
559
+ });
560
+ }
561
+ catch (error) {
562
+ const message = error instanceof Error ? error.message : String(error);
563
+ res
564
+ .status(message.includes("ETag mismatch") || message.includes("已存在") ? 409 : 400)
565
+ .json({ error: message });
566
+ }
567
+ });
447
568
  // Portable P4 bundle: graph + Prompt assets. Everything is kept inside one
448
569
  // display-name Workflow package; the runtime later uses that package root
449
570
  // as specDir, so @prompts/... is stable after export/import.
@@ -692,22 +813,20 @@ export async function createPipelineService(opts) {
692
813
  ? readJsonFile(files.manifest)
693
814
  : {};
694
815
  const source = manifest.source;
816
+ const publication = manifest.publication;
695
817
  res.json({
696
818
  id: req.params.id,
697
819
  spec: JSON.parse(raw),
698
820
  etag: etagOf(raw),
699
821
  updatedAt: toBeijingIso(fs.statSync(fp).mtime),
700
822
  version: latestWorkflowPackageVersion(workflowPackagePaths(wk.rootPath, req.params.id)),
701
- ...(typeof manifest.description === "string"
702
- ? { description: manifest.description }
703
- : {}),
823
+ ...(typeof manifest.description === "string" ? { description: manifest.description } : {}),
704
824
  ...(source &&
705
825
  typeof source === "object" &&
706
826
  !Array.isArray(source) &&
707
827
  typeof source.templateId === "string"
708
828
  ? {
709
- templateId: source
710
- .templateId,
829
+ templateId: source.templateId,
711
830
  ...(Number.isSafeInteger(source.templateVersion)
712
831
  ? {
713
832
  templateVersion: Number(source.templateVersion),
@@ -715,6 +834,19 @@ export async function createPipelineService(opts) {
715
834
  : {}),
716
835
  }
717
836
  : {}),
837
+ ...(publication &&
838
+ typeof publication === "object" &&
839
+ !Array.isArray(publication) &&
840
+ typeof publication.templateId === "string"
841
+ ? {
842
+ publishedTemplateId: publication.templateId,
843
+ ...(Number.isSafeInteger(publication.templateVersion)
844
+ ? {
845
+ publishedTemplateVersion: Number(publication.templateVersion),
846
+ }
847
+ : {}),
848
+ }
849
+ : {}),
718
850
  });
719
851
  });
720
852
  router.delete("/workspaces/:wid/workflows/:id", (req, res) => {
@@ -808,15 +940,14 @@ export async function createPipelineService(opts) {
808
940
  schemaVersion: 1,
809
941
  name: newId,
810
942
  description: typeof manifest.description === "string" ? manifest.description : "",
811
- version: Number.isSafeInteger(manifest.version)
812
- ? Number(manifest.version) + 1
813
- : 1,
943
+ version: Number.isSafeInteger(manifest.version) ? Number(manifest.version) + 1 : 1,
814
944
  ...(manifest.source && typeof manifest.source === "object"
815
945
  ? { source: manifest.source }
816
946
  : {}),
817
- createdAt: typeof manifest.createdAt === "string"
818
- ? manifest.createdAt
819
- : nowBeijing(),
947
+ ...(manifest.publication && typeof manifest.publication === "object"
948
+ ? { publication: manifest.publication }
949
+ : {}),
950
+ createdAt: typeof manifest.createdAt === "string" ? manifest.createdAt : nowBeijing(),
820
951
  updatedAt: nowBeijing(),
821
952
  });
822
953
  ensureWorkflowPromptFiles(targetFiles.root, result.data);
@@ -894,19 +1025,17 @@ export async function createPipelineService(opts) {
894
1025
  writePackageJsonAtomic(files.manifest, {
895
1026
  schemaVersion: 1,
896
1027
  name: req.params.id,
897
- description: typeof previousManifest.description === "string"
898
- ? previousManifest.description
899
- : "",
1028
+ description: typeof previousManifest.description === "string" ? previousManifest.description : "",
900
1029
  version: Number.isSafeInteger(previousManifest.version)
901
1030
  ? Number(previousManifest.version) + 1
902
1031
  : 1,
903
- ...(previousManifest.source &&
904
- typeof previousManifest.source === "object"
1032
+ ...(previousManifest.source && typeof previousManifest.source === "object"
905
1033
  ? { source: previousManifest.source }
906
1034
  : {}),
907
- createdAt: typeof previousManifest.createdAt === "string"
908
- ? previousManifest.createdAt
909
- : nowBeijing(),
1035
+ ...(previousManifest.publication && typeof previousManifest.publication === "object"
1036
+ ? { publication: previousManifest.publication }
1037
+ : {}),
1038
+ createdAt: typeof previousManifest.createdAt === "string" ? previousManifest.createdAt : nowBeijing(),
910
1039
  updatedAt: nowBeijing(),
911
1040
  });
912
1041
  const version = recordWorkflowPackageVersion(files, raw);
@@ -1023,9 +1152,7 @@ export async function createPipelineService(opts) {
1023
1152
  if (typeof body.nodeId !== "string" ||
1024
1153
  typeof body.kind !== "string" ||
1025
1154
  typeof body.content !== "string") {
1026
- res
1027
- .status(400)
1028
- .json({ error: "nodeId, kind and UTF-8 content are required" });
1155
+ res.status(400).json({ error: "nodeId, kind and UTF-8 content are required" });
1029
1156
  return;
1030
1157
  }
1031
1158
  try {
@@ -1039,9 +1166,7 @@ export async function createPipelineService(opts) {
1039
1166
  kind: body.kind,
1040
1167
  content: body.content,
1041
1168
  ...(typeof body.name === "string" ? { name: body.name } : {}),
1042
- ...(typeof body.mimeType === "string"
1043
- ? { mimeType: body.mimeType }
1044
- : {}),
1169
+ ...(typeof body.mimeType === "string" ? { mimeType: body.mimeType } : {}),
1045
1170
  ...(typeof body.runId === "string" ? { runId: body.runId } : {}),
1046
1171
  ...(typeof body.taskId === "string" ? { taskId: body.taskId } : {}),
1047
1172
  });
@@ -1178,8 +1303,7 @@ export async function createPipelineService(opts) {
1178
1303
  let resources = parsedInput.data.resources ?? [];
1179
1304
  for (const resource of resources) {
1180
1305
  const relativePath = resource.workspaceRelativePath;
1181
- if (relativePath !== undefined &&
1182
- !isSafeWorkspaceRelativePath(relativePath)) {
1306
+ if (relativePath !== undefined && !isSafeWorkspaceRelativePath(relativePath)) {
1183
1307
  res.status(400).json({
1184
1308
  error: "resource workspaceRelativePath must stay inside the workspace",
1185
1309
  });
@@ -1190,9 +1314,7 @@ export async function createPipelineService(opts) {
1190
1314
  if (resourceIds !== undefined) {
1191
1315
  if (!Array.isArray(resourceIds) ||
1192
1316
  resourceIds.some((id) => typeof id !== "string" || id.trim().length === 0)) {
1193
- res
1194
- .status(400)
1195
- .json({ error: "resourceIds must be a non-empty string array" });
1317
+ res.status(400).json({ error: "resourceIds must be a non-empty string array" });
1196
1318
  return;
1197
1319
  }
1198
1320
  // IDs are opaque to AgentStorm. Paseo or a Workflow-specific tool owns
@@ -1242,9 +1364,7 @@ export async function createPipelineService(opts) {
1242
1364
  ? { idempotencyKey: parsedInput.data.idempotencyKey }
1243
1365
  : {}),
1244
1366
  ...(resources.length > 0 ? { resources } : {}),
1245
- ...(Array.isArray(requestedTaskIds)
1246
- ? { taskIds: requestedTaskIds }
1247
- : {}),
1367
+ ...(Array.isArray(requestedTaskIds) ? { taskIds: requestedTaskIds } : {}),
1248
1368
  });
1249
1369
  res.status(202).json({
1250
1370
  run: started.run,
@@ -1357,9 +1477,7 @@ export async function createPipelineService(opts) {
1357
1477
  return;
1358
1478
  }
1359
1479
  const requestedOffset = Number.parseInt(String(req.query.from ?? "0"), 10);
1360
- const fromOffset = Number.isFinite(requestedOffset) && requestedOffset > 0
1361
- ? requestedOffset
1362
- : 0;
1480
+ const fromOffset = Number.isFinite(requestedOffset) && requestedOffset > 0 ? requestedOffset : 0;
1363
1481
  res.json(workspace.kernel.store.readPipelineEventsFromOffset(req.params.pipelineRunId, fromOffset));
1364
1482
  });
1365
1483
  router.get("/workspaces/:wid/agents/:agentId/pipelines/:pipelineRunId/approvals", (req, res) => {
@@ -1392,9 +1510,7 @@ export async function createPipelineService(opts) {
1392
1510
  ? Number(req.body.planRevision)
1393
1511
  : undefined;
1394
1512
  const comments = ApprovalCommentSchema.array().parse(req.body?.comments ?? []);
1395
- res.json(found.coordinator.approve(approvalId, typeof req.body?.decidedBy === "string"
1396
- ? req.body.decidedBy
1397
- : "user", planRevision, comments));
1513
+ res.json(found.coordinator.approve(approvalId, typeof req.body?.decidedBy === "string" ? req.body.decidedBy : "user", planRevision, comments));
1398
1514
  }
1399
1515
  catch (error) {
1400
1516
  res.status(409).json({
@@ -1420,9 +1536,7 @@ export async function createPipelineService(opts) {
1420
1536
  ? Number(req.body.planRevision)
1421
1537
  : undefined;
1422
1538
  const comments = ApprovalCommentSchema.array().parse(req.body?.comments ?? []);
1423
- res.json(found.coordinator.reject(approvalId, feedback, typeof req.body?.decidedBy === "string"
1424
- ? req.body.decidedBy
1425
- : "user", planRevision, comments));
1539
+ res.json(found.coordinator.reject(approvalId, feedback, typeof req.body?.decidedBy === "string" ? req.body.decidedBy : "user", planRevision, comments));
1426
1540
  }
1427
1541
  catch (error) {
1428
1542
  res.status(409).json({
@@ -1494,8 +1608,8 @@ export async function createPipelineService(opts) {
1494
1608
  const taskId = typeof req.body?.taskId === "string" ? req.body.taskId.trim() : "";
1495
1609
  const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId.trim() : "";
1496
1610
  const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
1497
- if (!taskId || !text) {
1498
- res.status(400).json({ error: "taskId and text are required" });
1611
+ if (!taskId) {
1612
+ res.status(400).json({ error: "taskId is required" });
1499
1613
  return;
1500
1614
  }
1501
1615
  // Older Paseo clients used the generic Pipeline resume endpoint for the
@@ -1503,7 +1617,7 @@ export async function createPipelineService(opts) {
1503
1617
  // keep that request on the original Plan Run instead of asking the
1504
1618
  // Execute task scope to resolve it.
1505
1619
  if (taskId.startsWith("planner:") || taskId === found.value.planRunId) {
1506
- if (!nodeId) {
1620
+ if (!nodeId || !text) {
1507
1621
  res.status(400).json({ error: "nodeId and text are required" });
1508
1622
  return;
1509
1623
  }
@@ -1524,7 +1638,7 @@ export async function createPipelineService(opts) {
1524
1638
  res.json(found.coordinator.resumeBlocked(req.params.pipelineRunId, {
1525
1639
  taskId,
1526
1640
  ...(nodeId ? { nodeId } : {}),
1527
- text,
1641
+ ...(text ? { text } : {}),
1528
1642
  }));
1529
1643
  }
1530
1644
  catch (error) {
@@ -1601,9 +1715,7 @@ export async function createPipelineService(opts) {
1601
1715
  const runId = typeof req.body?.runId === "string" ? req.body.runId : "";
1602
1716
  const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId : "";
1603
1717
  if (!runId || !nodeId || found.value.planRunId !== runId) {
1604
- res
1605
- .status(400)
1606
- .json({ error: "runId/nodeId do not match the Pipeline plan" });
1718
+ res.status(400).json({ error: "runId/nodeId do not match the Pipeline plan" });
1607
1719
  return;
1608
1720
  }
1609
1721
  try {
@@ -1638,9 +1750,7 @@ export async function createPipelineService(opts) {
1638
1750
  const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId : "";
1639
1751
  const outcome = typeof req.body?.outcome === "string" ? req.body.outcome.trim() : "";
1640
1752
  if (!runId || !nodeId || !outcome) {
1641
- res
1642
- .status(400)
1643
- .json({ error: "runId, nodeId and outcome are required" });
1753
+ res.status(400).json({ error: "runId, nodeId and outcome are required" });
1644
1754
  return;
1645
1755
  }
1646
1756
  try {
@@ -1672,9 +1782,7 @@ export async function createPipelineService(opts) {
1672
1782
  const runId = typeof req.body?.runId === "string" ? req.body.runId : "";
1673
1783
  const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId : "";
1674
1784
  const title = typeof req.body?.title === "string" ? req.body.title.trim() : "";
1675
- const instructions = typeof req.body?.instructions === "string"
1676
- ? req.body.instructions.trim()
1677
- : "";
1785
+ const instructions = typeof req.body?.instructions === "string" ? req.body.instructions.trim() : "";
1678
1786
  if (!runId || !nodeId || !title || !instructions) {
1679
1787
  res.status(400).json({
1680
1788
  error: "runId, nodeId, title and instructions are required",
@@ -1754,7 +1862,7 @@ export async function createPipelineService(opts) {
1754
1862
  }
1755
1863
  });
1756
1864
  function findPipelineRun(workspace, agentId, pipelineRunId) {
1757
- for (const [coordinatorKey, coordinator,] of workspace.coordinators.entries()) {
1865
+ for (const [coordinatorKey, coordinator] of workspace.coordinators.entries()) {
1758
1866
  // PipelineRunStore is shared by a Workspace, so a Coordinator can see
1759
1867
  // persisted runs belonging to another Main Agent. The map key is the
1760
1868
  // authoritative in-memory owner; do not return an old Agent's
@@ -1857,8 +1965,7 @@ export async function createPipelineService(opts) {
1857
1965
  // UI uses a synthetic `planner:<PipelineRunId>` id, which must not
1858
1966
  // be passed to Execute task-scope recovery. Re-open the original
1859
1967
  // Plan Run directly and keep its identity.
1860
- if (created.runKind === "plan" ||
1861
- (!created.runKind && !created.taskId)) {
1968
+ if (created.runKind === "plan" || (!created.runKind && !created.taskId)) {
1862
1969
  if (!nodeId || typeof text !== "string" || !text.trim()) {
1863
1970
  res.status(400).json({ error: "nodeId and text are required" });
1864
1971
  return;
@@ -2149,9 +2256,7 @@ export async function createPipelineService(opts) {
2149
2256
  res.json({ ok: true });
2150
2257
  }
2151
2258
  catch (e) {
2152
- res
2153
- .status(500)
2154
- .json({ error: e instanceof Error ? e.message : String(e) });
2259
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
2155
2260
  }
2156
2261
  });
2157
2262
  }
@@ -2221,9 +2326,7 @@ export async function createPipelineService(opts) {
2221
2326
  const runner = wk.taskRunners.get(req.params.agentId);
2222
2327
  if (runner)
2223
2328
  runner.pause();
2224
- const tasks = wk.kernel.tasks
2225
- .list()
2226
- .filter((t) => t.agentId === req.params.agentId);
2329
+ const tasks = wk.kernel.tasks.list().filter((t) => t.agentId === req.params.agentId);
2227
2330
  const runIds = [];
2228
2331
  for (const t of tasks) {
2229
2332
  if (t.runId) {
@@ -2240,9 +2343,7 @@ export async function createPipelineService(opts) {
2240
2343
  let wk = findKernelByAgentId(req.params.agentId);
2241
2344
  if (!wk)
2242
2345
  wk = kernels.get("default");
2243
- const tasks = wk.kernel.tasks
2244
- .list()
2245
- .filter((t) => t.agentId === req.params.agentId);
2346
+ const tasks = wk.kernel.tasks.list().filter((t) => t.agentId === req.params.agentId);
2246
2347
  const runIds = [];
2247
2348
  for (const t of tasks) {
2248
2349
  if (t.runId) {
@@ -2377,19 +2478,43 @@ function etagOf(raw) {
2377
2478
  // same JSON may not. Treat that formatting difference as the same document
2378
2479
  // so a freshly-created Workflow can be updated with the returned ETag.
2379
2480
  const canonical = raw.replace(/\r?\n$/, "");
2380
- return `"${createHash("sha256")
2381
- .update(canonical)
2382
- .digest("hex")
2383
- .slice(0, 16)}"`;
2481
+ return `"${createHash("sha256").update(canonical).digest("hex").slice(0, 16)}"`;
2482
+ }
2483
+ function readProjectWorkflowSnapshot(workspaceRoot, workflowId) {
2484
+ const files = workflowPackagePaths(workspaceRoot, workflowId);
2485
+ if (!fs.existsSync(files.workflow))
2486
+ throw new Error("workflow not found");
2487
+ const raw = fs.readFileSync(files.workflow, "utf8");
2488
+ const parsed = safeParseWorkflowSpec(JSON.parse(raw));
2489
+ if (!parsed.success)
2490
+ throw new Error("stored workflow is invalid");
2491
+ validateWorkflowPromptReferences(parsed.data);
2492
+ const now = nowBeijing();
2493
+ const storedManifest = fs.existsSync(files.manifest)
2494
+ ? readJsonFile(files.manifest)
2495
+ : undefined;
2496
+ const manifest = storedManifest ?? {
2497
+ schemaVersion: 1,
2498
+ name: workflowId,
2499
+ description: "",
2500
+ version: Math.max(1, latestWorkflowPackageVersion(files)),
2501
+ createdAt: now,
2502
+ updatedAt: now,
2503
+ };
2504
+ return {
2505
+ files,
2506
+ raw,
2507
+ spec: parsed.data,
2508
+ manifest,
2509
+ layout: fs.existsSync(files.layout) ? readJsonFile(files.layout) : {},
2510
+ assets: collectWorkflowPackageAssets(files.root),
2511
+ };
2384
2512
  }
2385
2513
  function isSafeWorkflowId(value) {
2386
2514
  // Workflow names are user-facing, so Unicode and spaces are deliberately
2387
2515
  // allowed. Only reject strings that could make the JSON file escape its
2388
2516
  // workspace-local .agentstorm/workflows directory.
2389
- return (value.length > 0 &&
2390
- value !== "." &&
2391
- value !== ".." &&
2392
- !/[\\/\0]/.test(value));
2517
+ return value.length > 0 && value !== "." && value !== ".." && !/[\\/\0]/.test(value);
2393
2518
  }
2394
2519
  /** Read-only compatibility for Workflow files written before Prompt and
2395
2520
  * Skills moved from graph nodes to the Agent registry. New saves never emit
@@ -2496,9 +2621,7 @@ export function workflowReadiness(rootPath, spec) {
2496
2621
  const reference = prompt.slice(1).replaceAll("\\", "/");
2497
2622
  const safe = safePackageRelativePath(reference);
2498
2623
  const expected = workflowPromptRelativePath(agentId).replaceAll("\\", "/");
2499
- if (!safe ||
2500
- !reference.startsWith("prompts/") ||
2501
- reference !== expected) {
2624
+ if (!safe || !reference.startsWith("prompts/") || reference !== expected) {
2502
2625
  missingPromptAssets.push({
2503
2626
  agentId,
2504
2627
  reference,
@@ -2526,8 +2649,7 @@ export function workflowReadiness(rootPath, spec) {
2526
2649
  }
2527
2650
  const realRoot = fs.realpathSync(root);
2528
2651
  const realTarget = fs.realpathSync(target);
2529
- if (realTarget !== realRoot &&
2530
- !realTarget.startsWith(`${realRoot}${path.sep}`)) {
2652
+ if (realTarget !== realRoot && !realTarget.startsWith(`${realRoot}${path.sep}`)) {
2531
2653
  missingPromptAssets.push({
2532
2654
  agentId,
2533
2655
  reference,
@@ -2617,9 +2739,7 @@ export async function workflowReadinessWithBackend(rootPath, spec, backend, skil
2617
2739
  }
2618
2740
  function isSafeWorkspaceRelativePath(value) {
2619
2741
  const normalized = value.replaceAll("\\", "/");
2620
- if (!normalized ||
2621
- normalized.startsWith("/") ||
2622
- /^[A-Za-z]:\//.test(normalized))
2742
+ if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized))
2623
2743
  return false;
2624
2744
  return normalized
2625
2745
  .split("/")
@@ -2646,7 +2766,7 @@ function readLegacyAgentWorkflowBindings(rootPath) {
2646
2766
  }
2647
2767
  }
2648
2768
  function sessionPathSegment(agentId) {
2649
- return encodeURIComponent(agentId).replace(/[.']/g, (char) => char === "." ? "%2E" : "%27");
2769
+ return encodeURIComponent(agentId).replace(/[.']/g, (char) => (char === "." ? "%2E" : "%27"));
2650
2770
  }
2651
2771
  function sessionWorkflowFiles(rootPath) {
2652
2772
  const sessionsRoot = path.join(rootPath, ".agentstorm", "sessions");
@@ -1,10 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { safeParseWorkflowSpec } from "@agentstorm/protocol";
6
6
  import { nowBeijing } from "@agentstorm/kernel";
7
- import { assertInsidePackage, assertWorkflowDisplayName, recordWorkflowPackageVersion, safePackageRelativePath, workflowPackageExists, workflowPackagePaths, workflowPromptRelativePath, writeJsonAtomic as writePackageJsonAtomic, } from "./workflow-package.js";
7
+ import { assertInsidePackage, assertWorkflowDisplayName, collectWorkflowPackageAssets, recordWorkflowPackageVersion, safePackageRelativePath, workflowPackageExists, workflowPackagePaths, workflowPromptRelativePath, writeJsonAtomic as writePackageJsonAtomic, } from "./workflow-package.js";
8
8
  const TEMPLATE_ROOT_NAME = "templates";
9
9
  const LEGACY_TEMPLATE_ROOT_NAME = "agentstorm/templates";
10
10
  const TEMPLATE_ROOT_MIGRATION_ID = "nested-template-root-v1";
@@ -48,17 +48,7 @@ function loadSeedTemplate(id) {
48
48
  const parsed = safeParseWorkflowSpec(spec);
49
49
  if (!parsed.success)
50
50
  throw new Error(`内置模板 ${id} 的 WorkflowSpec 无效`);
51
- const assets = {};
52
- for (const config of Object.values(parsed.data.agents)) {
53
- if (!config.prompt?.startsWith("@"))
54
- continue;
55
- const relative = safeRelativePath(config.prompt.slice(1));
56
- if (!relative)
57
- throw new Error(`内置模板 ${id} 的 Prompt 路径无效`);
58
- const target = path.resolve(dir, relative);
59
- assertInside(dir, target);
60
- assets[relative] = fs.readFileSync(target, "utf8");
61
- }
51
+ const assets = readTemplateAssets(dir, parsed.data);
62
52
  return {
63
53
  id,
64
54
  name: manifest.name,
@@ -170,8 +160,9 @@ export class GlobalTemplateLibrary {
170
160
  }
171
161
  materialize(id, workspaceRoot, options) {
172
162
  const template = this.get(id);
173
- const spec = applyAgentConfig(template.spec, options?.agentConfig);
174
- const workflowId = assertWorkflowDisplayName(options?.name?.trim() || spec.name || id);
163
+ const configuredSpec = applyAgentConfig(template.spec, options?.agentConfig);
164
+ const workflowId = assertWorkflowDisplayName(options?.name?.trim() || configuredSpec.name || id);
165
+ const spec = { ...configuredSpec, name: workflowId };
175
166
  if (workflowPackageExists(workspaceRoot, workflowId)) {
176
167
  throw new Error(`Workflow “${workflowId}” 已存在,请更换显示名称`);
177
168
  }
@@ -313,25 +304,42 @@ export class GlobalTemplateLibrary {
313
304
  }
314
305
  writeDocument(manifest, spec, layout, assets) {
315
306
  const dir = this.templateDir(manifest.id);
316
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
317
- writeJsonAtomic(path.join(dir, "workflow.json"), spec);
318
- writeJsonAtomic(path.join(dir, "layout.json"), layout);
319
- // Prompt files are one-to-one with the current Workflow nodes. Remove
320
- // stale Prompt files left by a node rename/delete before writing the new
321
- // asset set; otherwise an edited global template would silently retain
322
- // orphan files that are no longer part of its package.
323
- fs.rmSync(path.join(dir, "prompts"), { recursive: true, force: true });
324
- for (const [relative, content] of Object.entries(assets)) {
325
- const safe = safeRelativePath(relative);
326
- if (!safe || Buffer.byteLength(content, "utf8") > MAX_ASSET_BYTES) {
327
- throw new Error(`模板 Prompt 资源无效:${relative}`);
307
+ fs.mkdirSync(this.rootPath, { recursive: true, mode: 0o700 });
308
+ const stage = path.join(this.rootPath, `.template-${randomUUID()}.tmp`);
309
+ const backup = path.join(this.rootPath, `.template-${randomUUID()}.bak`);
310
+ let movedCurrent = false;
311
+ try {
312
+ fs.mkdirSync(stage, { recursive: true, mode: 0o700 });
313
+ writeJsonAtomic(path.join(stage, "workflow.json"), spec);
314
+ writeJsonAtomic(path.join(stage, "layout.json"), layout);
315
+ for (const [relative, content] of Object.entries(assets)) {
316
+ const safe = safeRelativePath(relative);
317
+ if (!safe ||
318
+ (!safe.startsWith(`prompts${path.sep}`) && !safe.startsWith(`assets${path.sep}`)) ||
319
+ Buffer.byteLength(content, "utf8") > MAX_ASSET_BYTES) {
320
+ throw new Error(`模板资源无效:${relative}`);
321
+ }
322
+ const target = path.resolve(stage, safe);
323
+ assertInside(stage, target);
324
+ fs.mkdirSync(path.dirname(target), { recursive: true });
325
+ fs.writeFileSync(target, content, "utf8");
326
+ }
327
+ writeJsonAtomic(path.join(stage, "manifest.json"), manifest);
328
+ if (fs.existsSync(dir)) {
329
+ fs.renameSync(dir, backup);
330
+ movedCurrent = true;
331
+ }
332
+ fs.renameSync(stage, dir);
333
+ if (movedCurrent)
334
+ fs.rmSync(backup, { recursive: true, force: true });
335
+ }
336
+ catch (error) {
337
+ fs.rmSync(stage, { recursive: true, force: true });
338
+ if (movedCurrent && !fs.existsSync(dir) && fs.existsSync(backup)) {
339
+ fs.renameSync(backup, dir);
328
340
  }
329
- const target = path.resolve(dir, safe);
330
- assertInside(dir, target);
331
- fs.mkdirSync(path.dirname(target), { recursive: true });
332
- fs.writeFileSync(target, content, "utf8");
341
+ throw error;
333
342
  }
334
- writeJsonAtomic(path.join(dir, "manifest.json"), manifest);
335
343
  }
336
344
  }
337
345
  function parseInput(input) {
@@ -347,6 +355,9 @@ function parseInput(input) {
347
355
  const relative = safeRelativePath(key);
348
356
  if (!relative)
349
357
  throw new Error(`模板资源路径无效:${key}`);
358
+ if (!relative.startsWith(`prompts${path.sep}`) && !relative.startsWith(`assets${path.sep}`)) {
359
+ throw new Error(`模板资源必须位于 prompts/ 或 assets/:${key}`);
360
+ }
350
361
  if (typeof value !== "string")
351
362
  throw new Error(`模板资源必须是 UTF-8 文本:${key}`);
352
363
  if (relative in assets)
@@ -423,7 +434,8 @@ function parseLayout(value) {
423
434
  return result;
424
435
  }
425
436
  function readTemplateAssets(dir, spec) {
426
- const assets = {};
437
+ const packageAssets = collectWorkflowPackageAssets(dir);
438
+ const assets = Object.fromEntries(Object.entries(packageAssets).filter(([relative]) => relative.startsWith("assets/")));
427
439
  for (const [agentId, config] of Object.entries(spec.agents)) {
428
440
  if (!config.prompt?.startsWith("@"))
429
441
  continue;
@@ -432,14 +444,10 @@ function readTemplateAssets(dir, spec) {
432
444
  if (!relative || !relative.startsWith(`prompts${path.sep}`) || relative !== expected) {
433
445
  throw new Error(`模板 Prompt 路径无效:${config.prompt}`);
434
446
  }
435
- if (relative in assets)
436
- continue;
437
- const target = path.resolve(dir, relative);
438
- assertInside(dir, target);
439
- if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
447
+ if (!(relative in packageAssets)) {
440
448
  throw new Error(`模板 Prompt 不存在:${relative}`);
441
449
  }
442
- assets[relative] = fs.readFileSync(target, "utf8");
450
+ assets[relative.split(path.sep).join("/")] = packageAssets[relative];
443
451
  }
444
452
  return assets;
445
453
  }
@@ -24,6 +24,6 @@ role: planner
24
24
 
25
25
  ## 计划结果
26
26
 
27
- 生成完整的 `PlanResult`:`revision`、`summary`、`tasks` 和 `resources`。每个 task 至少包含 `displayId`、`title`、`description`、`acceptanceCriteria` 与 `dependencyDisplayIds`。
27
+ 生成完整的 `PlanResult`:`revision`、`summary`、`tasks`、`removedTaskDisplayIds` 和 `resources`。每个 task 至少包含 `displayId`、`title`、`description`、`acceptanceCriteria` 与 `dependencyDisplayIds`。仅在重新规划时,才把明确需要删除且尚未启动的 pending 任务写入 `removedTaskDisplayIds`;正常规划使用空数组。
28
28
 
29
29
  计划正文应说明目标、范围、关键决策、风险、验收方式和执行顺序;结构化结果必须与正文一致。需要审批者查看的计划文件和补充材料通过 `resources` 明确列出。
@@ -30,7 +30,7 @@ role: planner
30
30
  ## 计划产物
31
31
 
32
32
  - 在当前节点的 `resourceRoot` 下生成 `sonic-plan.md`,包含需求覆盖关系、任务与依赖、验收标准、风险、验证策略和人工介入点。
33
- - 提交完整 PlanResult:`revision`、`summary`、`tasks`、`resources`。每个任务使用稳定且唯一的 `displayId`,并填写 `title`、`description`、`acceptanceCriteria`、`dependencyDisplayIds`;任务相关输入或交付文件放入任务级 `resources`。
33
+ - 提交完整 PlanResult:`revision`、`summary`、`tasks`、`removedTaskDisplayIds`、`resources`。每个任务使用稳定且唯一的 `displayId`,并填写 `title`、`description`、`acceptanceCriteria`、`dependencyDisplayIds`;任务相关输入或交付文件放入任务级 `resources`。仅在重新规划时,才把明确需要删除且尚未启动的 pending 任务写入 `removedTaskDisplayIds`;正常规划使用空数组。
34
34
  - 计划级 `resources` 必须包含节点 Summary、`sonic-plan.md`、已批准的 `requirements-analysis.md` 与 `prd.md`,以及审批者需要查看的关键证据。正文和结构化 PlanResult 必须一致。
35
35
  - 不创建旧版任务清单,不写死 Summary 路径,也不手工推进节点;AgentStorm 负责保存计划、创建任务、发起第二次人工审批和后续调度。
36
36
 
@@ -18,13 +18,33 @@ export interface WorkflowPackageManifest {
18
18
  description: string;
19
19
  version: number;
20
20
  source?: {
21
- kind: "blank" | "template" | "bundle";
22
- templateId?: string;
23
- templateVersion?: number;
21
+ kind: "blank";
22
+ } | {
23
+ kind: "template";
24
+ templateId: string;
25
+ templateVersion: number;
26
+ } | {
27
+ kind: "bundle";
28
+ } | {
29
+ kind: "copy";
30
+ workflowName: string;
31
+ workflowVersion: number;
32
+ };
33
+ publication?: {
34
+ templateId: string;
35
+ templateVersion: number;
24
36
  };
25
37
  createdAt: string;
26
38
  updatedAt: string;
27
39
  }
40
+ export interface WorkflowPackageSnapshotInput {
41
+ workspaceRoot: string;
42
+ name: string;
43
+ spec: unknown;
44
+ layout: unknown;
45
+ assets: Record<string, string>;
46
+ manifest: WorkflowPackageManifest;
47
+ }
28
48
  export interface WorkflowPackagePaths {
29
49
  root: string;
30
50
  manifest: string;
@@ -41,6 +61,13 @@ export declare function listWorkflowPackageNames(workspaceRoot: string): string[
41
61
  export declare function readJsonFile<T>(filePath: string): T;
42
62
  export declare function writeJsonAtomic(filePath: string, value: unknown): void;
43
63
  export declare function writeTextAtomic(filePath: string, value: string): void;
64
+ /**
65
+ * Write a complete project Workflow package and expose it with one directory
66
+ * rename. Runtime/session state is intentionally outside this input, and the
67
+ * new package always starts with one local Workflow version.
68
+ */
69
+ export declare function writeWorkflowPackageSnapshot(input: WorkflowPackageSnapshotInput): WorkflowPackagePaths;
70
+ export declare function collectWorkflowPackageAssets(packageRoot: string): Record<string, string>;
44
71
  export declare function safePackageRelativePath(value: string): string | null;
45
72
  export declare function assertInsidePackage(packageRoot: string, candidate: string): void;
46
73
  export declare function recordWorkflowPackageVersion(paths: WorkflowPackagePaths, raw: string): number;
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  /**
4
5
  * The on-disk contract for a project Workflow. A Workflow is one directory
5
6
  * named by its display name; executable graph, canvas metadata and Prompt
@@ -34,6 +35,17 @@ export function workflowPackagePaths(workspaceRoot, displayName) {
34
35
  versions: path.join(root, ".versions"),
35
36
  };
36
37
  }
38
+ function packagePathsFromRoot(root) {
39
+ return {
40
+ root,
41
+ manifest: path.join(root, "manifest.json"),
42
+ workflow: path.join(root, "workflow.json"),
43
+ layout: path.join(root, "layout.json"),
44
+ prompts: path.join(root, "prompts"),
45
+ assets: path.join(root, "assets"),
46
+ versions: path.join(root, ".versions"),
47
+ };
48
+ }
37
49
  export function workflowPackageExists(workspaceRoot, displayName) {
38
50
  const paths = workflowPackagePaths(workspaceRoot, displayName);
39
51
  return fs.existsSync(paths.root) && fs.statSync(paths.root).isDirectory();
@@ -42,8 +54,12 @@ export function listWorkflowPackageNames(workspaceRoot) {
42
54
  const root = path.join(workspaceRoot, WORKFLOW_PACKAGE_ROOT);
43
55
  if (!fs.existsSync(root))
44
56
  return [];
45
- return fs.readdirSync(root, { withFileTypes: true })
46
- .filter((entry) => entry.isDirectory() && entry.name !== "." && entry.name !== ".." && !entry.name.startsWith("."))
57
+ return fs
58
+ .readdirSync(root, { withFileTypes: true })
59
+ .filter((entry) => entry.isDirectory() &&
60
+ entry.name !== "." &&
61
+ entry.name !== ".." &&
62
+ !entry.name.startsWith("."))
47
63
  .map((entry) => entry.name)
48
64
  .filter((name) => {
49
65
  try {
@@ -69,6 +85,76 @@ export function writeTextAtomic(filePath, value) {
69
85
  fs.writeFileSync(tmp, value, "utf8");
70
86
  fs.renameSync(tmp, filePath);
71
87
  }
88
+ /**
89
+ * Write a complete project Workflow package and expose it with one directory
90
+ * rename. Runtime/session state is intentionally outside this input, and the
91
+ * new package always starts with one local Workflow version.
92
+ */
93
+ export function writeWorkflowPackageSnapshot(input) {
94
+ const name = assertWorkflowDisplayName(input.name);
95
+ const target = workflowPackagePaths(input.workspaceRoot, name);
96
+ if (fs.existsSync(target.root))
97
+ throw new Error(`Workflow “${name}” 已存在`);
98
+ const parent = path.dirname(target.root);
99
+ fs.mkdirSync(parent, { recursive: true });
100
+ const stageRoot = path.join(parent, `.workflow-${randomUUID()}.tmp`);
101
+ const stage = packagePathsFromRoot(stageRoot);
102
+ try {
103
+ fs.mkdirSync(stage.root, { recursive: true, mode: 0o700 });
104
+ for (const [relativePath, content] of Object.entries(input.assets)) {
105
+ const safe = safePackageRelativePath(relativePath);
106
+ if (!safe ||
107
+ (!safe.startsWith(`prompts${path.sep}`) && !safe.startsWith(`assets${path.sep}`))) {
108
+ throw new Error(`Workflow 资源路径无效:${relativePath}`);
109
+ }
110
+ const destination = path.resolve(stage.root, safe);
111
+ assertInsidePackage(stage.root, destination);
112
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
113
+ fs.writeFileSync(destination, content, "utf8");
114
+ }
115
+ writeJsonAtomic(stage.workflow, input.spec);
116
+ writeJsonAtomic(stage.layout, input.layout);
117
+ writeJsonAtomic(stage.manifest, input.manifest);
118
+ recordWorkflowPackageVersion(stage, JSON.stringify(input.spec, null, 2));
119
+ fs.renameSync(stage.root, target.root);
120
+ return target;
121
+ }
122
+ catch (error) {
123
+ fs.rmSync(stage.root, { recursive: true, force: true });
124
+ throw error;
125
+ }
126
+ }
127
+ export function collectWorkflowPackageAssets(packageRoot) {
128
+ const result = {};
129
+ for (const directory of ["prompts", "assets"]) {
130
+ const root = path.join(packageRoot, directory);
131
+ if (!fs.existsSync(root))
132
+ continue;
133
+ collectTextAssets(packageRoot, root, result);
134
+ }
135
+ return result;
136
+ }
137
+ function collectTextAssets(packageRoot, directory, result) {
138
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
139
+ const target = path.join(directory, entry.name);
140
+ const stat = fs.lstatSync(target);
141
+ if (stat.isSymbolicLink())
142
+ throw new Error(`Workflow 资源不能是符号链接:${entry.name}`);
143
+ if (stat.isDirectory()) {
144
+ collectTextAssets(packageRoot, target, result);
145
+ continue;
146
+ }
147
+ if (!stat.isFile())
148
+ throw new Error(`Workflow 资源类型无效:${entry.name}`);
149
+ if (stat.size > 8 * 1024 * 1024)
150
+ throw new Error(`Workflow 资源超过 8 MiB:${entry.name}`);
151
+ const relative = path.relative(packageRoot, target);
152
+ const safe = safePackageRelativePath(relative);
153
+ if (!safe)
154
+ throw new Error(`Workflow 资源路径无效:${relative}`);
155
+ result[safe.split(path.sep).join("/")] = fs.readFileSync(target, "utf8");
156
+ }
157
+ }
72
158
  export function safePackageRelativePath(value) {
73
159
  const normalized = value.replaceAll("\\", "/");
74
160
  if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized))
@@ -87,7 +173,8 @@ export function assertInsidePackage(packageRoot, candidate) {
87
173
  }
88
174
  export function recordWorkflowPackageVersion(paths, raw) {
89
175
  fs.mkdirSync(paths.versions, { recursive: true });
90
- const versions = fs.readdirSync(paths.versions)
176
+ const versions = fs
177
+ .readdirSync(paths.versions)
91
178
  .map((file) => /^(\d+)\.json$/.exec(file)?.[1])
92
179
  .filter(Boolean)
93
180
  .map(Number);
@@ -98,16 +185,21 @@ export function recordWorkflowPackageVersion(paths, raw) {
98
185
  export function latestWorkflowPackageVersion(paths) {
99
186
  if (!fs.existsSync(paths.versions))
100
187
  return 0;
101
- return fs.readdirSync(paths.versions)
188
+ return (fs
189
+ .readdirSync(paths.versions)
102
190
  .map((file) => /^(\d+)\.json$/.exec(file)?.[1])
103
191
  .filter(Boolean)
104
192
  .map(Number)
105
193
  .filter((version) => Number.isSafeInteger(version) && version > 0)
106
194
  .sort((a, b) => a - b)
107
- .at(-1) ?? 0;
195
+ .at(-1) ?? 0);
108
196
  }
109
197
  export function workflowPromptRelativePath(nodeId) {
110
- const safe = nodeId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
198
+ const safe = nodeId
199
+ .trim()
200
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
201
+ .replace(/^-+|-+$/g, "")
202
+ .toLowerCase();
111
203
  return path.join("prompts", `${safe || "agent"}.md`);
112
204
  }
113
205
  //# sourceMappingURL=workflow-package.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentstorm/server",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "AgentStorm resident Pipeline service",
5
5
  "files": [
6
6
  "dist"
@@ -19,9 +19,9 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@agentstorm/agent-runtime": "0.2.5",
23
- "@agentstorm/kernel": "0.2.5",
24
- "@agentstorm/protocol": "0.2.5",
22
+ "@agentstorm/agent-runtime": "0.2.6",
23
+ "@agentstorm/kernel": "0.2.6",
24
+ "@agentstorm/protocol": "0.2.6",
25
25
  "express": "^4.21.0"
26
26
  },
27
27
  "license": "AGPL-3.0-or-later",