@desplega.ai/agent-swarm 1.102.0 → 1.103.0

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.
Files changed (107) hide show
  1. package/dist/{actions-j9399ktm.js → actions-7txktrdj.js} +6 -6
  2. package/dist/{app-tnq1pz27.js → app-xm3xsdye.js} +3 -3
  3. package/dist/{assistant-c7y92gkw.js → assistant-qpv63wk6.js} +9 -9
  4. package/dist/{boot-reembed-k7e6fc1p.js → boot-reembed-9cxqka3e.js} +3 -3
  5. package/dist/{boot-reembed-b97r699t.js → boot-reembed-chxhrwv8.js} +4 -4
  6. package/dist/{boot-scrub-logs-14kbc61y.js → boot-scrub-logs-8qtfkcz8.js} +2 -2
  7. package/dist/{claude-managed-adapter-g07dbz3b.js → claude-managed-adapter-z29hjgzz.js} +3 -3
  8. package/dist/{cli-tsz1q7gk.js → cli-2307phk1.js} +375 -23
  9. package/dist/{cli-g5px3d7x.js → cli-2957pqpj.js} +24 -1
  10. package/dist/{cli-9qascwj7.js → cli-2qynerth.js} +1 -1
  11. package/dist/{cli-k1ghyj8b.js → cli-4fnp2tc3.js} +1 -1
  12. package/dist/{cli-hc4rd6sv.js → cli-85pf4z38.js} +1 -1
  13. package/dist/{cli-zk4gn40w.js → cli-8hyv6c2v.js} +6 -14
  14. package/dist/{cli-k81y1cjb.js → cli-8w95v19d.js} +1 -1
  15. package/dist/{cli-7xdq3jg3.js → cli-92fqyjcv.js} +1 -1
  16. package/dist/{cli-zctkpdmd.js → cli-dz3d6zjg.js} +1 -1
  17. package/dist/{cli-d0t4854c.js → cli-fedn86nz.js} +213 -476
  18. package/dist/{cli-g861fwjp.js → cli-fwxdt7kt.js} +3 -3
  19. package/dist/{cli-2aaa9mqr.js → cli-fygaw191.js} +2 -2
  20. package/dist/{cli-qbc6mncp.js → cli-gzepjz54.js} +2 -2
  21. package/dist/{cli-r68m8arr.js → cli-jt9d9fjw.js} +1 -1
  22. package/dist/{cli-axtg1zaa.js → cli-khe4x17a.js} +1 -1
  23. package/dist/{cli-sygq3fhn.js → cli-kk51f3tf.js} +5 -5
  24. package/dist/{cli-dgab5x2h.js → cli-krvxq8bc.js} +1 -1
  25. package/dist/{cli-xde8vhhp.js → cli-ksq38x4n.js} +2 -2
  26. package/dist/{cli-stqqf5kh.js → cli-m8f6qzck.js} +1 -1
  27. package/dist/{cli-ap4md0gc.js → cli-ns0r7mkr.js} +30 -5
  28. package/dist/{cli-t2jk33ta.js → cli-rttgde5f.js} +18 -6
  29. package/dist/{cli-mhj07xbq.js → cli-rvae030h.js} +3 -3
  30. package/dist/{cli-q6jzg6wj.js → cli-s14sb64w.js} +4 -4
  31. package/dist/{cli-zmf7refz.js → cli-v9283nma.js} +1 -1
  32. package/dist/{cli-fjx4x7nn.js → cli-xmtsan9k.js} +1 -1
  33. package/dist/{cli-vgck9mqq.js → cli-yeemwevj.js} +155 -37
  34. package/dist/{cli-5bqa41fd.js → cli-yfwwjeft.js} +1 -1
  35. package/dist/{cli-s8jw5bmg.js → cli-yp2krx5r.js} +2 -2
  36. package/dist/cli.js +9 -9
  37. package/dist/{codex-adapter-wf119mnw.js → codex-adapter-w0xharsg.js} +3 -3
  38. package/dist/{codex-session-runner-1gknw7mh.js → codex-session-runner-kzbjcm2k.js} +3 -3
  39. package/dist/{commands-2wyn3swb.js → commands-aqn63mcw.js} +2 -2
  40. package/dist/{db-567ntpxv.js → db-azrvwsnj.js} +2 -2
  41. package/dist/{handlers-wvwpk2kj.js → handlers-hf61har3.js} +9 -9
  42. package/dist/{hook-fatkywc0.js → hook-306p3yz4.js} +2 -2
  43. package/dist/{http-eq6wxve6.js → http-zts4haq7.js} +81 -62
  44. package/dist/{index-400x6p3j.js → index-29vg51x6.js} +6 -6
  45. package/dist/{index-9wqh5352.js → index-3c651yfk.js} +10 -10
  46. package/dist/{index-6aj8966p.js → index-bfh9hgek.js} +5 -5
  47. package/dist/{index-d1rm4ea3.js → index-h2yqjyk0.js} +8 -8
  48. package/dist/{keepalive-6x9nx92g.js → keepalive-a0dg5pds.js} +4 -4
  49. package/dist/{lead-5fgqwe13.js → lead-7mcxedkc.js} +21 -21
  50. package/dist/{maintenance-eajx5k1k.js → maintenance-mrzqd53t.js} +4 -4
  51. package/dist/{onboard-jc1w1edg.js → onboard-4dkpv9wa.js} +2 -2
  52. package/dist/{otel-impl-8k375amf.js → otel-impl-jt7gpyp9.js} +1 -1
  53. package/dist/{pi-mono-adapter-hf8sw7hs.js → pi-mono-adapter-zf7dt1wg.js} +1 -1
  54. package/dist/{pricing-refresh-zed6916t.js → pricing-refresh-xqnzh8dt.js} +4 -4
  55. package/dist/{seed-pricing-5805qzkz.js → seed-pricing-56tjm1fj.js} +3 -3
  56. package/dist/{setup-wz05vxh0.js → setup-tbwk8j10.js} +2 -2
  57. package/dist/{worker-j359graf.js → worker-e03za92t.js} +21 -21
  58. package/openapi.json +70 -1
  59. package/package.json +1 -1
  60. package/src/be/db.ts +83 -13
  61. package/src/be/migrations/098_repo_hooks_config.sql +3 -0
  62. package/src/be/task-lifecycle-events.ts +42 -0
  63. package/src/commands/runner.ts +174 -25
  64. package/src/github/task-reactions.ts +21 -0
  65. package/src/hooks/tool-loop-detection.test.ts +40 -0
  66. package/src/hooks/tool-loop-detection.ts +37 -1
  67. package/src/http/agents.ts +7 -0
  68. package/src/http/index.ts +16 -2
  69. package/src/http/prompt-templates.ts +1 -1
  70. package/src/http/repos.ts +5 -1
  71. package/src/http/schedules.ts +1 -1
  72. package/src/http/session-data.ts +2 -0
  73. package/src/http/tasks.ts +2 -1
  74. package/src/http/workflows.ts +1 -0
  75. package/src/prompts/resolver.ts +1 -1
  76. package/src/scheduler/scheduler.ts +25 -2
  77. package/src/server-runtime-counters.ts +12 -0
  78. package/src/server-user.ts +1 -2
  79. package/src/server.ts +5 -0
  80. package/src/telemetry.ts +18 -1
  81. package/src/tests/http-api-integration.test.ts +4 -0
  82. package/src/tests/model-control.test.ts +6 -6
  83. package/src/tests/swarm-repos.test.ts +25 -2
  84. package/src/tests/task-lifecycle-events.test.ts +53 -0
  85. package/src/tests/task-lifecycle-telemetry.test.ts +37 -1
  86. package/src/tests/telemetry-init.test.ts +32 -0
  87. package/src/tools/prompt-templates/preview.ts +1 -1
  88. package/src/tools/repos/update-repo.ts +6 -1
  89. package/src/tools/schedules/create-schedule.ts +1 -1
  90. package/src/tools/schedules/update-schedule.ts +1 -1
  91. package/src/tools/send-task.ts +7 -2
  92. package/src/tools/task-action.ts +6 -2
  93. package/src/types.ts +155 -1
  94. package/src/utils/template.ts +127 -0
  95. package/src/workflows/engine.ts +7 -0
  96. package/src/workflows/executors/agent-task.ts +1 -2
  97. package/src/workflows/template.ts +12 -121
  98. package/src/workflows/triggers.ts +6 -2
  99. package/templates/community/code-health-reports/PLAYBOOK.md +355 -0
  100. package/templates/community/code-health-reports/README.md +38 -0
  101. package/templates/community/code-health-reports/lead-prompt.md +52 -0
  102. package/templates/community/code-health-reports/report.mjs +378 -0
  103. package/templates/community/code-health-reports/run.sh +143 -0
  104. package/templates/schedules/weekly-code-health-reports/config.json +13 -0
  105. package/templates/schedules/weekly-code-health-reports/content.md +60 -0
  106. package/src/model-tiers.ts +0 -140
  107. package/templates/community/.gitkeep +0 -0
package/src/types.ts CHANGED
@@ -1,5 +1,152 @@
1
1
  import * as z from "zod";
2
- import { ModelTierSchema } from "./model-tiers";
2
+
3
+ // ─── Model Tiers ─────────────────────────────────────────────────────────────
4
+ // Merged from the former `src/model-tiers.ts` to dissolve the benign
5
+ // `types` ↔ `model-tiers` import cycle (cycle-break #1; prep for the
6
+ // `@swarm/types` monorepo package). `ModelTierSchema` is a runtime VALUE that is
7
+ // referenced later in this file (e.g. the task/schedule schemas), so this
8
+ // section must stay above its first use. `ProviderName` is referenced only as a
9
+ // TYPE below; it is declared further down this same module and type references
10
+ // are hoisted, so the forward reference is safe.
11
+
12
+ export const ModelTierSchema = z.enum(["smol", "regular", "smart", "ultra"]);
13
+ export type ModelTier = z.infer<typeof ModelTierSchema>;
14
+
15
+ export const MODEL_TIERS = ModelTierSchema.options;
16
+
17
+ export const LEGACY_MODEL_TO_TIER: Record<string, ModelTier> = {
18
+ haiku: "smol",
19
+ sonnet: "regular",
20
+ opus: "smart",
21
+ fable: "ultra",
22
+ };
23
+
24
+ export const MODEL_TIER_LABELS: Record<ModelTier, string> = {
25
+ smol: "Smol",
26
+ regular: "Regular",
27
+ smart: "Smart",
28
+ ultra: "Ultra",
29
+ };
30
+
31
+ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, string>> = {
32
+ claude: {
33
+ smol: "haiku",
34
+ regular: "sonnet",
35
+ smart: "opus",
36
+ ultra: "fable",
37
+ },
38
+ "claude-managed": {
39
+ smol: "claude-haiku-4-5",
40
+ regular: "claude-sonnet-4-6",
41
+ smart: "claude-opus-4-8",
42
+ ultra: "claude-fable-5",
43
+ },
44
+ codex: {
45
+ smol: "gpt-5.4-mini",
46
+ regular: "gpt-5.4",
47
+ smart: "gpt-5.5",
48
+ ultra: "gpt-5.5",
49
+ },
50
+ pi: {
51
+ smol: "openrouter/deepseek/deepseek-v4-flash",
52
+ regular: "openrouter/deepseek/deepseek-v4-flash",
53
+ smart: "openrouter/deepseek/deepseek-v4-pro",
54
+ ultra: "openrouter/anthropic/claude-opus-4.8",
55
+ },
56
+ opencode: {
57
+ smol: "openrouter/deepseek/deepseek-v4-flash",
58
+ regular: "openrouter/deepseek/deepseek-v4-flash",
59
+ smart: "openrouter/deepseek/deepseek-v4-pro",
60
+ ultra: "openrouter/anthropic/claude-opus-4.8",
61
+ },
62
+ devin: {
63
+ smol: "devin",
64
+ regular: "devin",
65
+ smart: "devin",
66
+ ultra: "devin",
67
+ },
68
+ };
69
+
70
+ export function parseModelTier(value: string | null | undefined): ModelTier | undefined {
71
+ if (!value) return undefined;
72
+ const normalized = value.trim().toLowerCase();
73
+ return ModelTierSchema.safeParse(normalized).success
74
+ ? (normalized as ModelTier)
75
+ : LEGACY_MODEL_TO_TIER[normalized];
76
+ }
77
+
78
+ export function splitLegacyModelAlias(input: {
79
+ model?: string | null;
80
+ modelTier?: string | null;
81
+ }): { model?: string; modelTier?: ModelTier } {
82
+ const explicitTier = parseModelTier(input.modelTier);
83
+ const model = input.model?.trim();
84
+ if (!model) return { modelTier: explicitTier };
85
+
86
+ const legacyTier = parseModelTier(model);
87
+ if (legacyTier && !explicitTier) {
88
+ return { modelTier: legacyTier };
89
+ }
90
+
91
+ return {
92
+ model,
93
+ modelTier: explicitTier,
94
+ };
95
+ }
96
+
97
+ function parseTierMapJson(value: string | undefined): Partial<Record<ModelTier, string>> {
98
+ if (!value) return {};
99
+ try {
100
+ const parsed = JSON.parse(value) as unknown;
101
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
102
+ const result: Partial<Record<ModelTier, string>> = {};
103
+ for (const tier of MODEL_TIERS) {
104
+ const model = (parsed as Record<string, unknown>)[tier];
105
+ if (typeof model === "string" && model.trim()) result[tier] = model.trim();
106
+ }
107
+ return result;
108
+ } catch {
109
+ return {};
110
+ }
111
+ }
112
+
113
+ export function resolveModelTier(opts: {
114
+ tier?: string | null;
115
+ harnessProvider: ProviderName;
116
+ env?: Record<string, string | undefined>;
117
+ }): string | undefined {
118
+ const tier = parseModelTier(opts.tier);
119
+ if (!tier) return undefined;
120
+
121
+ const env = opts.env ?? {};
122
+ const jsonOverrides = parseTierMapJson(env.MODEL_TIER_MAP);
123
+ const envKey = `MODEL_TIER_${tier.toUpperCase()}`;
124
+ const directOverride = env[envKey]?.trim();
125
+ if (directOverride) return directOverride;
126
+ if (jsonOverrides[tier]) return jsonOverrides[tier];
127
+
128
+ return DEFAULT_MODEL_TIER_MAP[opts.harnessProvider]?.[tier];
129
+ }
130
+
131
+ export function resolveTaskModelSelection(opts: {
132
+ model?: string | null;
133
+ modelTier?: string | null;
134
+ harnessProvider: ProviderName;
135
+ env?: Record<string, string | undefined>;
136
+ }): { model?: string; source: "model" | "modelTier" | "none" } {
137
+ const model = opts.model?.trim();
138
+ if (model) return { model, source: "model" };
139
+
140
+ const tierModel = resolveModelTier({
141
+ tier: opts.modelTier,
142
+ harnessProvider: opts.harnessProvider,
143
+ env: opts.env,
144
+ });
145
+ if (tierModel) return { model: tierModel, source: "modelTier" };
146
+
147
+ return { source: "none" };
148
+ }
149
+ // ─── End Model Tiers ─────────────────────────────────────────────────────────
3
150
 
4
151
  // Task status - includes new unassigned and offered states
5
152
  export const AgentTaskStatusSchema = z.enum([
@@ -962,6 +1109,12 @@ export const RepoGuidelinesSchema = z.object({
962
1109
 
963
1110
  export type RepoGuidelines = z.infer<typeof RepoGuidelinesSchema>;
964
1111
 
1112
+ export const RepoHooksSchema = z.object({
1113
+ enabled: z.boolean().default(false),
1114
+ });
1115
+
1116
+ export type RepoHooks = z.infer<typeof RepoHooksSchema>;
1117
+
965
1118
  export const SwarmRepoSchema = z.object({
966
1119
  id: z.string().uuid(),
967
1120
  url: z.string().min(1),
@@ -969,6 +1122,7 @@ export const SwarmRepoSchema = z.object({
969
1122
  clonePath: z.string().min(1),
970
1123
  defaultBranch: z.string().default("main"),
971
1124
  autoClone: z.boolean().default(true),
1125
+ hooks: RepoHooksSchema.optional().default({ enabled: false }),
972
1126
  guidelines: RepoGuidelinesSchema.nullable().optional(),
973
1127
  createdAt: z.string(),
974
1128
  lastUpdatedAt: z.string(),
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Replace {{path.to.value}} tokens in a template string
3
+ * with values from the context object.
4
+ *
5
+ * Returns the interpolated result and a list of any unresolved token paths.
6
+ *
7
+ * Lives in `utils/` (not `workflows/`) so any domain — prompts, worker
8
+ * commands, HTTP handlers, MCP tools — can interpolate template strings
9
+ * without taking a dependency on the workflow engine module. The workflow
10
+ * engine re-exports these from `@/workflows/template` for its own use.
11
+ */
12
+
13
+ export interface InterpolateResult {
14
+ result: string;
15
+ unresolved: string[];
16
+ }
17
+
18
+ export interface DeepInterpolateOptions {
19
+ preserveRawTokens?: boolean;
20
+ }
21
+
22
+ export function interpolate(template: string, ctx: Record<string, unknown>): InterpolateResult {
23
+ const unresolved: string[] = [];
24
+ const result = template.replace(/\{\{([^}]+)\}\}/g, (_match, path: string) => {
25
+ const keys = path.trim().split(".");
26
+ let value: unknown = ctx;
27
+ for (const key of keys) {
28
+ if (value == null || typeof value !== "object") {
29
+ unresolved.push(path.trim());
30
+ return "";
31
+ }
32
+ value = (value as Record<string, unknown>)[key];
33
+ }
34
+ if (value == null) {
35
+ unresolved.push(path.trim());
36
+ return "";
37
+ }
38
+ return typeof value === "object" ? safeStringify(value) : String(value);
39
+ });
40
+ return { result, unresolved };
41
+ }
42
+
43
+ /**
44
+ * Circular-reference-safe JSON.stringify for interpolation.
45
+ * Returns "[Circular]" instead of throwing on circular structures.
46
+ */
47
+ function safeStringify(value: unknown): string {
48
+ try {
49
+ return JSON.stringify(value);
50
+ } catch {
51
+ return "[Circular]";
52
+ }
53
+ }
54
+
55
+ /** Matches a string that is EXACTLY one {{path}} token with no surrounding text. */
56
+ const EXACT_TOKEN_RE = /^\{\{([^}]+)\}\}$/;
57
+
58
+ /**
59
+ * Resolve a dot-separated path against a context object.
60
+ * Returns `{ found: true, value }` on success, `{ found: false }` when any
61
+ * segment is missing or the traversal hits a non-object.
62
+ */
63
+ function resolvePath(
64
+ path: string,
65
+ ctx: Record<string, unknown>,
66
+ ): { found: true; value: unknown } | { found: false } {
67
+ const keys = path.trim().split(".");
68
+ let value: unknown = ctx;
69
+ for (const key of keys) {
70
+ if (value == null || typeof value !== "object") return { found: false };
71
+ value = (value as Record<string, unknown>)[key];
72
+ }
73
+ if (value === undefined) return { found: false };
74
+ return { found: true, value };
75
+ }
76
+
77
+ /**
78
+ * Deep-interpolate an arbitrary value tree (objects, arrays, strings).
79
+ *
80
+ * When `preserveRawTokens` is true and a string value is **exactly** one
81
+ * `{{path}}` token with no surrounding text, the resolved value is returned
82
+ * as-is (preserving object / array / number / boolean types). This is the
83
+ * "raw injection" path used by `swarm-script` node `config.args`.
84
+ *
85
+ * When a string contains multiple tokens or surrounding text (e.g.
86
+ * `"prefix-{{x}}"`) the existing string-interpolation path is used so the
87
+ * result remains a string.
88
+ *
89
+ * Non-string leaves are passed through unchanged.
90
+ */
91
+ export function deepInterpolate(
92
+ value: unknown,
93
+ ctx: Record<string, unknown>,
94
+ options: DeepInterpolateOptions = {},
95
+ ): { value: unknown; unresolved: string[] } {
96
+ const allUnresolved: string[] = [];
97
+
98
+ function walk(v: unknown): unknown {
99
+ if (typeof v === "string") {
100
+ const exactMatch = options.preserveRawTokens ? EXACT_TOKEN_RE.exec(v) : null;
101
+ if (exactMatch?.[1]) {
102
+ const path = exactMatch[1].trim();
103
+ const resolved = resolvePath(path, ctx);
104
+ if (!resolved.found) {
105
+ allUnresolved.push(path);
106
+ return "";
107
+ }
108
+ return resolved.value;
109
+ }
110
+ // Multi-token or mixed string - fall back to string interpolation.
111
+ const { result, unresolved } = interpolate(v, ctx);
112
+ allUnresolved.push(...unresolved);
113
+ return result;
114
+ }
115
+ if (Array.isArray(v)) return v.map(walk);
116
+ if (v != null && typeof v === "object") {
117
+ const out: Record<string, unknown> = {};
118
+ for (const [k, val] of Object.entries(v)) {
119
+ out[k] = walk(val);
120
+ }
121
+ return out;
122
+ }
123
+ return v;
124
+ }
125
+
126
+ return { value: walk(value), unresolved: allUnresolved };
127
+ }
@@ -10,6 +10,7 @@ import {
10
10
  updateWorkflowRun,
11
11
  updateWorkflowRunStep,
12
12
  } from "../be/db";
13
+ import { telemetry } from "../telemetry";
13
14
  import type { Workflow, WorkflowDefinition, WorkflowNode } from "../types";
14
15
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
15
16
  import { shouldSkipCooldown } from "./cooldown";
@@ -27,6 +28,7 @@ const MAX_STEPS_PER_RUN = Number(process.env.WORKFLOW_MAX_STEPS_PER_RUN) || 500;
27
28
 
28
29
  export interface WorkflowExecutionOptions {
29
30
  requestedByUserId?: string;
31
+ triggerType?: "schedule" | "manual" | "event" | "api";
30
32
  }
31
33
 
32
34
  /**
@@ -78,6 +80,11 @@ export async function startWorkflowExecution(
78
80
 
79
81
  const runId = crypto.randomUUID();
80
82
  createWorkflowRun({ id: runId, workflowId: workflow.id, triggerData });
83
+ telemetry.workflow("started", {
84
+ workflowId: workflow.id,
85
+ nodeCount: workflow.definition.nodes.length,
86
+ triggerType: options.triggerType ?? "manual",
87
+ });
81
88
 
82
89
  // Resolve inputs and merge into initial context
83
90
  const ctx: Record<string, unknown> = { trigger: triggerData };
@@ -1,9 +1,8 @@
1
1
  import { z } from "zod";
2
- import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
3
2
  import { workflowContextKey } from "../../tasks/context-key";
4
3
  import { withSiblingAwareness } from "../../tasks/sibling-awareness";
5
4
  import type { ExecutorMeta } from "../../types";
6
- import { FollowUpConfigSchema } from "../../types";
5
+ import { FollowUpConfigSchema, ModelTierSchema, splitLegacyModelAlias } from "../../types";
7
6
  import type { ExecutorResult } from "./base";
8
7
  import { BaseExecutor } from "./base";
9
8
 
@@ -1,122 +1,13 @@
1
1
  /**
2
- * Replace {{path.to.value}} tokens in a template string
3
- * with values from the context object.
4
- *
5
- * Returns the interpolated result and a list of any unresolved token paths.
6
- */
7
-
8
- export interface InterpolateResult {
9
- result: string;
10
- unresolved: string[];
11
- }
12
-
13
- export interface DeepInterpolateOptions {
14
- preserveRawTokens?: boolean;
15
- }
16
-
17
- export function interpolate(template: string, ctx: Record<string, unknown>): InterpolateResult {
18
- const unresolved: string[] = [];
19
- const result = template.replace(/\{\{([^}]+)\}\}/g, (_match, path: string) => {
20
- const keys = path.trim().split(".");
21
- let value: unknown = ctx;
22
- for (const key of keys) {
23
- if (value == null || typeof value !== "object") {
24
- unresolved.push(path.trim());
25
- return "";
26
- }
27
- value = (value as Record<string, unknown>)[key];
28
- }
29
- if (value == null) {
30
- unresolved.push(path.trim());
31
- return "";
32
- }
33
- return typeof value === "object" ? safeStringify(value) : String(value);
34
- });
35
- return { result, unresolved };
36
- }
37
-
38
- /**
39
- * Circular-reference-safe JSON.stringify for interpolation.
40
- * Returns "[Circular]" instead of throwing on circular structures.
41
- */
42
- function safeStringify(value: unknown): string {
43
- try {
44
- return JSON.stringify(value);
45
- } catch {
46
- return "[Circular]";
47
- }
48
- }
49
-
50
- /** Matches a string that is EXACTLY one {{path}} token with no surrounding text. */
51
- const EXACT_TOKEN_RE = /^\{\{([^}]+)\}\}$/;
52
-
53
- /**
54
- * Resolve a dot-separated path against a context object.
55
- * Returns `{ found: true, value }` on success, `{ found: false }` when any
56
- * segment is missing or the traversal hits a non-object.
57
- */
58
- function resolvePath(
59
- path: string,
60
- ctx: Record<string, unknown>,
61
- ): { found: true; value: unknown } | { found: false } {
62
- const keys = path.trim().split(".");
63
- let value: unknown = ctx;
64
- for (const key of keys) {
65
- if (value == null || typeof value !== "object") return { found: false };
66
- value = (value as Record<string, unknown>)[key];
67
- }
68
- if (value === undefined) return { found: false };
69
- return { found: true, value };
70
- }
71
-
72
- /**
73
- * Deep-interpolate an arbitrary value tree (objects, arrays, strings).
74
- *
75
- * When `preserveRawTokens` is true and a string value is **exactly** one
76
- * `{{path}}` token with no surrounding text, the resolved value is returned
77
- * as-is (preserving object / array / number / boolean types). This is the
78
- * "raw injection" path used by `swarm-script` node `config.args`.
79
- *
80
- * When a string contains multiple tokens or surrounding text (e.g.
81
- * `"prefix-{{x}}"`) the existing string-interpolation path is used so the
82
- * result remains a string.
83
- *
84
- * Non-string leaves are passed through unchanged.
85
- */
86
- export function deepInterpolate(
87
- value: unknown,
88
- ctx: Record<string, unknown>,
89
- options: DeepInterpolateOptions = {},
90
- ): { value: unknown; unresolved: string[] } {
91
- const allUnresolved: string[] = [];
92
-
93
- function walk(v: unknown): unknown {
94
- if (typeof v === "string") {
95
- const exactMatch = options.preserveRawTokens ? EXACT_TOKEN_RE.exec(v) : null;
96
- if (exactMatch?.[1]) {
97
- const path = exactMatch[1].trim();
98
- const resolved = resolvePath(path, ctx);
99
- if (!resolved.found) {
100
- allUnresolved.push(path);
101
- return "";
102
- }
103
- return resolved.value;
104
- }
105
- // Multi-token or mixed string - fall back to string interpolation.
106
- const { result, unresolved } = interpolate(v, ctx);
107
- allUnresolved.push(...unresolved);
108
- return result;
109
- }
110
- if (Array.isArray(v)) return v.map(walk);
111
- if (v != null && typeof v === "object") {
112
- const out: Record<string, unknown> = {};
113
- for (const [k, val] of Object.entries(v)) {
114
- out[k] = walk(val);
115
- }
116
- return out;
117
- }
118
- return v;
119
- }
120
-
121
- return { value: walk(value), unresolved: allUnresolved };
122
- }
2
+ * The `{{token}}` interpolation helpers moved to `@/utils/template` so that
3
+ * non-workflow domains (prompts, worker commands, HTTP handlers, MCP tools)
4
+ * can use them without depending on the workflow engine module.
5
+ *
6
+ * This module re-exports them for the workflow engine and existing tests.
7
+ */
8
+ export {
9
+ type DeepInterpolateOptions,
10
+ deepInterpolate,
11
+ type InterpolateResult,
12
+ interpolate,
13
+ } from "@/utils/template";
@@ -113,7 +113,9 @@ export async function handleWebhookTrigger(
113
113
  // string so non-JSON webhooks don't break.
114
114
  const triggerData = parseTriggerPayload(payload);
115
115
 
116
- const runId = await startWorkflowExecution(workflow, triggerData, registry);
116
+ const runId = await startWorkflowExecution(workflow, triggerData, registry, {
117
+ triggerType: "event",
118
+ });
117
119
  return { runId };
118
120
  }
119
121
 
@@ -152,7 +154,9 @@ export async function handleScheduleTrigger(
152
154
  scheduleName: schedule.name,
153
155
  firedAt: new Date().toISOString(),
154
156
  };
155
- const runId = await startWorkflowExecution(workflow, triggerData, registry);
157
+ const runId = await startWorkflowExecution(workflow, triggerData, registry, {
158
+ triggerType: "schedule",
159
+ });
156
160
  runIds.push(runId);
157
161
  console.log(
158
162
  `[Triggers] Schedule "${schedule.name}" triggered workflow "${workflow.name}" (run: ${runId})`,