@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.
- package/dist/pipeline-service.d.ts +2 -0
- package/dist/pipeline-service.js +220 -100
- package/dist/template-library.js +47 -39
- package/dist/templates/general-workflow/prompts/planner.md +1 -1
- package/dist/templates/sonic-pipeline/prompts/planner.md +1 -1
- package/dist/workflow-package.d.ts +30 -3
- package/dist/workflow-package.js +98 -6
- package/package.json +4 -4
|
@@ -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";
|
package/dist/pipeline-service.js
CHANGED
|
@@ -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
|
-
|
|
818
|
-
? manifest.
|
|
819
|
-
:
|
|
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
|
-
|
|
908
|
-
? previousManifest.
|
|
909
|
-
:
|
|
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
|
|
1498
|
-
res.status(400).json({ error: "taskId
|
|
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
|
|
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
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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
|
|
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");
|
package/dist/template-library.js
CHANGED
|
@@ -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
|
|
174
|
-
const workflowId = assertWorkflowDisplayName(options?.name?.trim() ||
|
|
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(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
package/dist/workflow-package.js
CHANGED
|
@@ -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
|
|
46
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
23
|
-
"@agentstorm/kernel": "0.2.
|
|
24
|
-
"@agentstorm/protocol": "0.2.
|
|
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",
|