@desplega.ai/agent-swarm 1.79.4 → 1.80.1

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 (130) hide show
  1. package/openapi.json +496 -32
  2. package/package.json +14 -6
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +102 -31
  5. package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
  6. package/src/be/migrations/064_scripts.sql +39 -0
  7. package/src/be/migrations/065_script_embeddings.sql +7 -0
  8. package/src/be/pricing-normalize.ts +81 -0
  9. package/src/be/scripts/db.ts +391 -0
  10. package/src/be/scripts/embeddings.ts +231 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +193 -0
  13. package/src/be/seed-pricing.ts +293 -0
  14. package/src/cli.tsx +22 -5
  15. package/src/commands/artifact.ts +3 -2
  16. package/src/commands/claude-managed-setup.ts +21 -4
  17. package/src/commands/codex-login.ts +5 -3
  18. package/src/commands/onboard.tsx +2 -1
  19. package/src/commands/runner.ts +663 -246
  20. package/src/commands/setup.tsx +5 -3
  21. package/src/hooks/hook.ts +4 -3
  22. package/src/http/context.ts +6 -2
  23. package/src/http/index.ts +126 -68
  24. package/src/http/memory.ts +28 -0
  25. package/src/http/openapi.ts +1 -0
  26. package/src/http/page-proxy.ts +2 -1
  27. package/src/http/route-def.ts +1 -0
  28. package/src/http/schedules.ts +37 -0
  29. package/src/http/scripts.ts +381 -0
  30. package/src/http/session-data.ts +74 -23
  31. package/src/linear/outbound.ts +9 -2
  32. package/src/otel-impl.ts +200 -0
  33. package/src/otel.ts +132 -0
  34. package/src/providers/claude-adapter.ts +52 -6
  35. package/src/providers/claude-managed-adapter.ts +43 -17
  36. package/src/providers/claude-managed-pricing.ts +34 -0
  37. package/src/providers/codex-adapter.ts +38 -27
  38. package/src/providers/codex-models.ts +22 -3
  39. package/src/providers/devin-adapter.ts +11 -0
  40. package/src/providers/opencode-adapter.ts +31 -7
  41. package/src/providers/pi-mono-adapter.ts +39 -7
  42. package/src/providers/pricing-sources.md +52 -0
  43. package/src/providers/swarm-events-shared.ts +8 -4
  44. package/src/providers/types.ts +33 -10
  45. package/src/scripts-runtime/ctx.ts +23 -0
  46. package/src/scripts-runtime/eval-harness.ts +39 -0
  47. package/src/scripts-runtime/executors/native.ts +229 -0
  48. package/src/scripts-runtime/executors/registry.ts +16 -0
  49. package/src/scripts-runtime/executors/types.ts +63 -0
  50. package/src/scripts-runtime/extract-signature.ts +81 -0
  51. package/src/scripts-runtime/import-allowlist.ts +109 -0
  52. package/src/scripts-runtime/loader.ts +96 -0
  53. package/src/scripts-runtime/redacted.ts +48 -0
  54. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  55. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  56. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  57. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  58. package/src/scripts-runtime/stdlib/index.ts +16 -0
  59. package/src/scripts-runtime/stdlib/table.ts +17 -0
  60. package/src/scripts-runtime/swarm-config.ts +35 -0
  61. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  62. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  63. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  64. package/src/server.ts +18 -0
  65. package/src/tests/api-key.test.ts +33 -0
  66. package/src/tests/claude-managed-adapter.test.ts +17 -3
  67. package/src/tests/claude-managed-setup.test.ts +10 -1
  68. package/src/tests/codex-adapter.test.ts +20 -19
  69. package/src/tests/codex-login.test.ts +1 -1
  70. package/src/tests/context-snapshot.test.ts +2 -2
  71. package/src/tests/context-window.test.ts +65 -1
  72. package/src/tests/devin-adapter.test.ts +2 -0
  73. package/src/tests/http/context-routes.test.ts +161 -0
  74. package/src/tests/linear-outbound-sync.test.ts +109 -0
  75. package/src/tests/mcp-tools.test.ts +69 -0
  76. package/src/tests/migration-063-schema-relax.test.ts +109 -0
  77. package/src/tests/opencode-adapter.test.ts +146 -1
  78. package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
  79. package/src/tests/pages-view-count.test.ts +30 -5
  80. package/src/tests/providers/codex-cost.test.ts +18 -0
  81. package/src/tests/providers/opencode-cost.test.ts +74 -0
  82. package/src/tests/providers/pi-cost.test.ts +128 -0
  83. package/src/tests/redacted.test.ts +29 -0
  84. package/src/tests/runner-tool-spans.test.ts +268 -0
  85. package/src/tests/script-executor-conformance.test.ts +142 -0
  86. package/src/tests/script-executor-registry.test.ts +17 -0
  87. package/src/tests/scripts-db.test.ts +329 -0
  88. package/src/tests/scripts-embeddings.test.ts +291 -0
  89. package/src/tests/scripts-extract-signature.test.ts +47 -0
  90. package/src/tests/scripts-http.test.ts +350 -0
  91. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  92. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  93. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  94. package/src/tests/scripts-runtime.test.ts +289 -0
  95. package/src/tests/sdk-allowlist.test.ts +59 -0
  96. package/src/tests/secret-scrubber.test.ts +54 -1
  97. package/src/tests/session-costs-codex-recompute.test.ts +35 -22
  98. package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
  99. package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
  100. package/src/tests/store-progress-cost.test.ts +6 -1
  101. package/src/tests/swarm-config.test.ts +38 -0
  102. package/src/tests/tool-annotations.test.ts +2 -2
  103. package/src/tests/tool-call-progress.test.ts +30 -0
  104. package/src/tests/workflow-e2e.test.ts +218 -0
  105. package/src/tests/workflow-executors.test.ts +32 -2
  106. package/src/tests/workflow-input-redaction.test.ts +232 -0
  107. package/src/tests/workflow-swarm-script.test.ts +273 -0
  108. package/src/tools/memory-rate.ts +2 -1
  109. package/src/tools/script-common.ts +88 -0
  110. package/src/tools/script-delete.ts +35 -0
  111. package/src/tools/script-query-types.ts +37 -0
  112. package/src/tools/script-run.ts +43 -0
  113. package/src/tools/script-search.ts +32 -0
  114. package/src/tools/script-upsert.ts +43 -0
  115. package/src/tools/store-progress.ts +16 -60
  116. package/src/tools/tool-config.ts +7 -0
  117. package/src/tools/utils.ts +65 -12
  118. package/src/types.ts +122 -10
  119. package/src/utils/api-key.ts +28 -0
  120. package/src/utils/context-window.ts +104 -4
  121. package/src/utils/page-session.ts +8 -6
  122. package/src/utils/secret-scrubber.ts +29 -1
  123. package/src/workflows/engine.ts +12 -4
  124. package/src/workflows/executors/index.ts +1 -0
  125. package/src/workflows/executors/registry.ts +2 -0
  126. package/src/workflows/executors/script.ts +12 -1
  127. package/src/workflows/executors/swarm-script.ts +170 -0
  128. package/src/workflows/input.ts +65 -0
  129. package/src/workflows/recovery.ts +31 -3
  130. package/src/workflows/resume.ts +43 -5
@@ -154,6 +154,13 @@ export const DEFERRED_TOOLS = new Set([
154
154
  "kv-incr",
155
155
  "kv-list",
156
156
 
157
+ // Reusable scripts (5)
158
+ "script-search",
159
+ "script-run",
160
+ "script-upsert",
161
+ "script-delete",
162
+ "script-query-types",
163
+
157
164
  // Other (3)
158
165
  "cancel-task",
159
166
  "inject-learning",
@@ -12,6 +12,8 @@ import type {
12
12
  ServerRequest,
13
13
  ToolAnnotations,
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
+ import { withSpan } from "../otel";
16
+ import { scrubSecrets } from "../utils/secret-scrubber";
15
17
 
16
18
  type Meta = RequestHandlerExtra<ServerRequest, ServerNotification>;
17
19
 
@@ -46,6 +48,38 @@ export const getRequestInfo = (req: Meta): RequestInfo => {
46
48
  };
47
49
  };
48
50
 
51
+ const PREVIEW_LIMIT = 500;
52
+
53
+ function previewValue(value: unknown): string | undefined {
54
+ if (value === undefined) return undefined;
55
+ try {
56
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
57
+ if (!serialized) return undefined;
58
+ const scrubbed = scrubSecrets(serialized);
59
+ return scrubbed.length > PREVIEW_LIMIT ? `${scrubbed.slice(0, PREVIEW_LIMIT)}...` : scrubbed;
60
+ } catch {
61
+ return "[unserializable]";
62
+ }
63
+ }
64
+
65
+ function toolRequestAttributes(name: string, requestInfo: RequestInfo, args?: unknown) {
66
+ return {
67
+ "mcp.tool.name": name,
68
+ "mcp.session.id": requestInfo.sessionId,
69
+ "agent.id": requestInfo.agentId,
70
+ "agentswarm.task.id": requestInfo.sourceTaskId,
71
+ "agentswarm.tool.args_preview": previewValue(args),
72
+ };
73
+ }
74
+
75
+ function toolResultAttributes(result: CallToolResult) {
76
+ return {
77
+ "mcp.tool.result_content_count": Array.isArray(result.content) ? result.content.length : 0,
78
+ "mcp.tool.is_error": result.isError ?? false,
79
+ "agentswarm.tool.result_preview": previewValue(result.content),
80
+ };
81
+ }
82
+
49
83
  // Infer the input type from the schema
50
84
  type InferInput<Args extends undefined | ZodRawShapeCompat | AnySchema> =
51
85
  Args extends ZodRawShapeCompat
@@ -104,23 +138,42 @@ export const createToolRegistrar = (server: McpServer) => {
104
138
  // When inputSchema is undefined, the MCP SDK calls handler(extra) with a single arg.
105
139
  // When inputSchema is defined, it calls handler(args, extra) with two args.
106
140
  if (config.inputSchema === undefined) {
107
- return server.registerTool(name, config, ((meta: Meta) => {
141
+ return server.registerTool(name, config, (async (meta: Meta) => {
108
142
  const requestInfo = getRequestInfo(meta);
109
- return (
110
- cb as (requestInfo: RequestInfo, meta: Meta) => CallToolResult | Promise<CallToolResult>
111
- )(requestInfo, meta);
143
+ return withSpan(
144
+ "mcp.tool",
145
+ async (span) => {
146
+ const result = await (
147
+ cb as (
148
+ requestInfo: RequestInfo,
149
+ meta: Meta,
150
+ ) => CallToolResult | Promise<CallToolResult>
151
+ )(requestInfo, meta);
152
+ span.setAttributes(toolResultAttributes(result));
153
+ return result;
154
+ },
155
+ toolRequestAttributes(name, requestInfo),
156
+ );
112
157
  }) as Parameters<typeof server.registerTool>[2]);
113
158
  }
114
159
 
115
- return server.registerTool(name, config, ((args: InferInput<InputArgs>, meta: Meta) => {
160
+ return server.registerTool(name, config, (async (args: InferInput<InputArgs>, meta: Meta) => {
116
161
  const requestInfo = getRequestInfo(meta);
117
- return (
118
- cb as (
119
- args: InferInput<InputArgs>,
120
- requestInfo: RequestInfo,
121
- meta: Meta,
122
- ) => CallToolResult | Promise<CallToolResult>
123
- )(args, requestInfo, meta);
162
+ return withSpan(
163
+ "mcp.tool",
164
+ async (span) => {
165
+ const result = await (
166
+ cb as (
167
+ args: InferInput<InputArgs>,
168
+ requestInfo: RequestInfo,
169
+ meta: Meta,
170
+ ) => CallToolResult | Promise<CallToolResult>
171
+ )(args, requestInfo, meta);
172
+ span.setAttributes(toolResultAttributes(result));
173
+ return result;
174
+ },
175
+ toolRequestAttributes(name, requestInfo, args),
176
+ );
124
177
  }) as Parameters<typeof server.registerTool>[2]);
125
178
  };
126
179
  };
package/src/types.ts CHANGED
@@ -192,7 +192,10 @@ export const AgentTaskSchema = z.object({
192
192
  // Context usage aggregates
193
193
  compactionCount: z.number().int().min(0).optional(),
194
194
  peakContextPercent: z.number().min(0).max(100).optional(),
195
- totalContextTokensUsed: z.number().int().min(0).optional(),
195
+ // Migration 063: renamed from totalContextTokensUsed. Semantic is now a
196
+ // monotonic max across the task's snapshots — "high water mark" rather than
197
+ // "latest known".
198
+ peakContextTokens: z.number().int().min(0).optional(),
196
199
  contextWindowSize: z.number().int().min(0).optional(),
197
200
 
198
201
  // Credential tracking
@@ -574,7 +577,9 @@ export const SessionLogSchema = z.object({
574
577
  export type SessionLog = z.infer<typeof SessionLogSchema>;
575
578
 
576
579
  // Session Cost Types (aggregated cost data per session)
577
- export const SessionCostSourceSchema = z.enum(["harness", "pricing-table"]);
580
+ // Migration 063 widened the set to include 'unpriced' for cases where the API
581
+ // recompute path couldn't find pricing rows for the (provider, model, token_class).
582
+ export const SessionCostSourceSchema = z.enum(["harness", "pricing-table", "unpriced"]);
578
583
  export type SessionCostSource = z.infer<typeof SessionCostSourceSchema>;
579
584
 
580
585
  export const SessionCostSchema = z.object({
@@ -587,13 +592,22 @@ export const SessionCostSchema = z.object({
587
592
  outputTokens: z.number().int().min(0).default(0),
588
593
  cacheReadTokens: z.number().int().min(0).default(0),
589
594
  cacheWriteTokens: z.number().int().min(0).default(0),
595
+ // Migration 063: reasoning_output_tokens from codex turn.completed events.
596
+ reasoningOutputTokens: z.number().int().min(0).default(0),
597
+ // Migration 063: thinking_input_tokens from claude extended-thinking flows.
598
+ thinkingTokens: z.number().int().min(0).default(0),
590
599
  durationMs: z.number().int().min(0),
591
- numTurns: z.number().int().min(1),
600
+ // numTurns is nullable — some adapters (e.g. Claude when num_turns is absent)
601
+ // can't honestly report a turn count. We prefer null over a faked 1.
602
+ numTurns: z.number().int().min(1).nullable(),
592
603
  model: z.string(),
593
604
  isError: z.boolean().default(false),
594
- // Phase 6: where the recorded totalCostUsd came from. New rows write the
595
- // actual source ('pricing-table' when the API recomputed Codex USD from DB
596
- // pricing rows, 'harness' otherwise). Defaults to 'harness' for back-compat.
605
+ // Phase 6 (extended by migration 063): where the recorded totalCostUsd came from.
606
+ // 'harness' value reported by the harness as-is.
607
+ // 'pricing-table' — value recomputed by the API from `pricing` rows.
608
+ // 'unpriced' — the API tried to recompute but the (provider, model)
609
+ // had no matching pricing rows; totalCostUsd is whatever
610
+ // the worker submitted (often 0).
597
611
  costSource: SessionCostSourceSchema.default("harness"),
598
612
  createdAt: z.iso.datetime(),
599
613
  });
@@ -646,6 +660,8 @@ export const EventNameSchema = z.enum([
646
660
  "system.boot",
647
661
  "system.migration",
648
662
  "system.error",
663
+ // Script catalog events
664
+ "script.global_upsert",
649
665
  ]);
650
666
 
651
667
  export const SwarmEventSchema = z.object({
@@ -862,18 +878,30 @@ export const StepValidationConfigSchema = z.object({
862
878
  });
863
879
  export type StepValidationConfig = z.infer<typeof StepValidationConfigSchema>;
864
880
 
881
+ export const SwarmScriptNodeConfigSchema = z.object({
882
+ scriptName: z.string().min(1),
883
+ scope: z.enum(["global", "agent"]).optional(),
884
+ pinHash: z.string().min(1).optional(),
885
+ args: z.record(z.string(), z.unknown()).optional(),
886
+ fsMode: z.enum(["none", "workspace-rw"]).optional(),
887
+ });
888
+ export type SwarmScriptNodeConfig = z.infer<typeof SwarmScriptNodeConfigSchema>;
889
+
865
890
  // --- Workflow Node (nodes-with-next) ---
866
891
 
867
892
  export const WorkflowNodeSchema = z.object({
868
893
  id: z.string().describe("Unique node identifier, used in 'next' and 'inputs' mappings"),
869
894
  type: z
870
895
  .string()
871
- .describe("Executor type: 'agent-task', 'script', 'raw-llm', 'validate', 'property-match'"),
896
+ .describe(
897
+ "Executor type: 'agent-task', 'script', 'swarm-script', 'raw-llm', 'validate', 'property-match'",
898
+ ),
872
899
  label: z.string().optional().describe("Human-readable label for UI display"),
873
900
  config: z
874
901
  .record(z.string(), z.unknown())
875
902
  .describe(
876
903
  "Executor-specific config. For agent-task: { template, outputSchema?, agentId?, tags?, priority?, dir?, vcsRepo?, model? }. " +
904
+ "For swarm-script: { scriptName, scope?, pinHash?, args?, fsMode? }. " +
877
905
  "Values support {{interpolation}} from the node's inputs context. " +
878
906
  "NOTE: config.outputSchema on agent-task nodes validates the AGENT's raw JSON output, " +
879
907
  "while node-level outputSchema validates the EXECUTOR's return value ({taskId, taskOutput}).",
@@ -1273,6 +1301,51 @@ export const PromptTemplateHistorySchema = z.object({
1273
1301
  });
1274
1302
  export type PromptTemplateHistory = z.infer<typeof PromptTemplateHistorySchema>;
1275
1303
 
1304
+ // ============================================================================
1305
+ // Script Types
1306
+ // ============================================================================
1307
+
1308
+ export const ScriptScopeSchema = z.enum(["global", "agent"]);
1309
+ export type ScriptScope = z.infer<typeof ScriptScopeSchema>;
1310
+
1311
+ export const ScriptFsModeSchema = z.enum(["none", "workspace-rw"]);
1312
+ export type ScriptFsMode = z.infer<typeof ScriptFsModeSchema>;
1313
+
1314
+ export const ScriptRecordSchema = z.object({
1315
+ id: z.string(),
1316
+ name: z.string(),
1317
+ scope: ScriptScopeSchema,
1318
+ scopeId: z.string().nullable(),
1319
+ source: z.string(),
1320
+ description: z.string(),
1321
+ intent: z.string(),
1322
+ signatureJson: z.string(),
1323
+ contentHash: z.string(),
1324
+ version: z.number(),
1325
+ isScratch: z.boolean(),
1326
+ typeChecked: z.boolean(),
1327
+ fsMode: ScriptFsModeSchema,
1328
+ createdByAgentId: z.string().nullable(),
1329
+ createdAt: z.string(),
1330
+ updatedAt: z.string(),
1331
+ });
1332
+ export type ScriptRecord = z.infer<typeof ScriptRecordSchema>;
1333
+
1334
+ export const ScriptVersionRecordSchema = z.object({
1335
+ id: z.string(),
1336
+ scriptId: z.string(),
1337
+ version: z.number(),
1338
+ source: z.string(),
1339
+ description: z.string(),
1340
+ intent: z.string(),
1341
+ signatureJson: z.string(),
1342
+ contentHash: z.string(),
1343
+ changedByAgentId: z.string().nullable(),
1344
+ changedAt: z.string(),
1345
+ changeReason: z.string().nullable(),
1346
+ });
1347
+ export type ScriptVersionRecord = z.infer<typeof ScriptVersionRecordSchema>;
1348
+
1276
1349
  // ============================================================================
1277
1350
  // Skill Types
1278
1351
  // ============================================================================
@@ -1381,6 +1454,21 @@ export type McpServerWithInstallInfo = z.infer<typeof McpServerWithInstallInfoSc
1381
1454
  export const ContextSnapshotEventTypeSchema = z.enum(["progress", "compaction", "completion"]);
1382
1455
  export type ContextSnapshotEventType = z.infer<typeof ContextSnapshotEventTypeSchema>;
1383
1456
 
1457
+ // Migration 063: the formula the emitting adapter used to compute
1458
+ // contextUsedTokens. Lets downstream consumers (UI badges, cross-provider
1459
+ // comparisons) reason about whether two numbers are commensurable. Values
1460
+ // match the inline doc in `src/be/migrations/063_cost_context_schema_relax.sql`.
1461
+ export const ContextFormulaSchema = z.enum([
1462
+ "input-cache-output", // unified formula (post-Phase 9)
1463
+ "input-cache-no-output", // pre-unification claude formula
1464
+ "input-output-no-cache", // pre-unification claude-managed formula
1465
+ "peak-proxy", // pre-unification codex formula
1466
+ "pi-delegated", // numbers come from the pi-ai SDK
1467
+ "harness-reported", // numbers come from a harness API (devin)
1468
+ "unknown", // pre-migration backfill or adapter didn't tag
1469
+ ]);
1470
+ export type ContextFormula = z.infer<typeof ContextFormulaSchema>;
1471
+
1384
1472
  export const ContextSnapshotSchema = z.object({
1385
1473
  id: z.uuid(),
1386
1474
  taskId: z.uuid(),
@@ -1396,13 +1484,18 @@ export const ContextSnapshotSchema = z.object({
1396
1484
  eventType: ContextSnapshotEventTypeSchema,
1397
1485
 
1398
1486
  // Compaction-specific (null for non-compaction)
1399
- compactTrigger: z.enum(["auto", "manual"]).optional(),
1487
+ compactTrigger: z.enum(["auto", "manual", "auto-inferred"]).optional(),
1400
1488
  preCompactTokens: z.number().int().min(0).optional(),
1401
1489
 
1402
1490
  // Cumulative counters at this point
1403
1491
  cumulativeInputTokens: z.number().int().min(0).default(0),
1404
1492
  cumulativeOutputTokens: z.number().int().min(0).default(0),
1405
1493
 
1494
+ // Migration 063 — adapter stamps the formula it used to compute
1495
+ // contextUsedTokens. Optional so old rows / new providers without a tag
1496
+ // don't break, but every adapter should populate this going forward.
1497
+ contextFormula: ContextFormulaSchema.optional(),
1498
+
1406
1499
  createdAt: z.iso.datetime(),
1407
1500
  });
1408
1501
 
@@ -1430,10 +1523,29 @@ export const BudgetSchema = z.object({
1430
1523
  });
1431
1524
  export type Budget = z.infer<typeof BudgetSchema>;
1432
1525
 
1433
- export const PricingProviderSchema = z.enum(["claude", "codex", "pi"]);
1526
+ // Migration 063 widened both enums and dropped the SQL CHECKs to match.
1527
+ // New providers can land without an accompanying schema migration; Zod is now
1528
+ // the single source of truth for what's a valid (provider, token_class) row.
1529
+ export const PricingProviderSchema = z.enum([
1530
+ "claude",
1531
+ "claude-managed",
1532
+ "codex",
1533
+ "pi",
1534
+ "opencode",
1535
+ "devin",
1536
+ "gemini",
1537
+ ]);
1434
1538
  export type PricingProvider = z.infer<typeof PricingProviderSchema>;
1435
1539
 
1436
- export const PricingTokenClassSchema = z.enum(["input", "cached_input", "output"]);
1540
+ export const PricingTokenClassSchema = z.enum([
1541
+ "input",
1542
+ "cached_input",
1543
+ "output",
1544
+ // Migration 063 additions:
1545
+ "cache_write", // claude / claude-managed cache creation
1546
+ "runtime_hour", // claude-managed runtime fee per hour
1547
+ "acu", // devin Agent Compute Unit
1548
+ ]);
1437
1549
  export type PricingTokenClass = z.infer<typeof PricingTokenClassSchema>;
1438
1550
 
1439
1551
  export const PricingRowSchema = z.object({
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Centralized resolution of the swarm API key from the environment.
3
+ *
4
+ * Precedence:
5
+ * 1. AGENT_SWARM_API_KEY (preferred — namespaced, safe to set globally)
6
+ * 2. API_KEY (legacy — kept for back-compat with existing setups)
7
+ *
8
+ * All swarm code (CLI, server, hooks, worker, scripts) must read the key via
9
+ * `getApiKey()` so a user can configure either env var and have it work end
10
+ * to end. Direct access to `process.env.API_KEY` / `process.env.AGENT_SWARM_API_KEY`
11
+ * outside this module is enforced against by `scripts/check-api-key-boundary.sh`.
12
+ */
13
+
14
+ type EnvLike = Record<string, string | undefined>;
15
+
16
+ export function getApiKey(env: EnvLike = process.env): string {
17
+ return env.AGENT_SWARM_API_KEY ?? env.API_KEY ?? "";
18
+ }
19
+
20
+ /**
21
+ * Mirror a resolved key onto both env var names so any downstream code that
22
+ * still reads the raw env (third-party libraries, spawned subprocesses that
23
+ * inherit env, etc.) sees a consistent value.
24
+ */
25
+ export function setApiKey(key: string, env: EnvLike = process.env): void {
26
+ env.AGENT_SWARM_API_KEY = key;
27
+ env.API_KEY = key;
28
+ }
@@ -2,31 +2,95 @@
2
2
  * Context window size lookup and usage computation utilities.
3
3
  *
4
4
  * This module is safe for both API and worker code — it has NO database imports.
5
+ *
6
+ * Phase 4 + Phase 9 of the cost-tracking plan:
7
+ * - `getContextWindowSize` now resolves shortnames, family-versioned ids
8
+ * (`claude-sonnet-4-6`), AND dated full ids (`claude-sonnet-4-6-20251004`)
9
+ * by stripping the trailing date suffix. Previously the dated form fell
10
+ * to the 200k default — wildly wrong for sonnet/opus 4.x.
11
+ * - `computeContextUsedUnified` is the canonical formula every adapter
12
+ * should use when emitting a `context_usage` event:
13
+ * contextUsedTokens = input + cache_read + cache_create + output
14
+ * The matching `CONTEXT_FORMULA` constant is what adapters stamp onto
15
+ * the snapshot's `contextFormula` field.
16
+ * - The legacy `computeContextUsed` stays for back-compat reads but is
17
+ * deprecated; new code should use `computeContextUsedUnified`.
18
+ */
19
+
20
+ /**
21
+ * Phase 9: stamp this onto every `context_usage` event the adapter emits.
22
+ * Callers that compute their own number for legacy reasons (e.g. pi-mono
23
+ * delegates to the pi-ai SDK) use a different value — see `ContextFormula`
24
+ * in `src/types.ts`.
5
25
  */
26
+ export const CONTEXT_FORMULA = "input-cache-output" as const;
6
27
 
7
28
  const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
+ // Anthropic 4.x family
30
+ "claude-opus-4-7": 1_000_000,
8
31
  "claude-opus-4-6": 1_000_000,
32
+ "claude-opus-4-5": 1_000_000,
33
+ "claude-opus-4-1": 200_000,
34
+ "claude-opus-4-0": 200_000,
9
35
  "claude-sonnet-4-6": 1_000_000,
36
+ "claude-sonnet-4-5": 1_000_000,
37
+ "claude-sonnet-4-0": 200_000,
10
38
  "claude-haiku-4-5": 200_000,
39
+ // Anthropic 3.x family (legacy)
40
+ "claude-3-7-sonnet": 200_000,
41
+ "claude-3-5-sonnet": 200_000,
42
+ "claude-3-5-haiku": 200_000,
43
+ "claude-3-opus": 200_000,
44
+ "claude-3-sonnet": 200_000,
45
+ "claude-3-haiku": 200_000,
46
+ // Shortnames used by the local-CLI adapter and pi-mono OpenRouter mirror.
11
47
  opus: 1_000_000,
12
48
  sonnet: 1_000_000,
13
49
  haiku: 200_000,
14
50
  default: 200_000,
15
51
  };
16
52
 
53
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
54
+
17
55
  /**
18
- * Look up the context window size (in tokens) for a given model identifier.
19
- * Falls back to the "default" entry when the model is not explicitly mapped.
56
+ * Strip a trailing date suffix from a Claude model id so dated full ids
57
+ * resolve to the same window as the family-versioned id.
58
+ *
59
+ * `claude-sonnet-4-6-20251004` → `claude-sonnet-4-6`
60
+ * `claude-haiku-4-5-20251001` → `claude-haiku-4-5`
61
+ *
62
+ * Anthropic's dated full ids are always `${family}-${major}-${minor}-${YYYYMMDD}`,
63
+ * so an 8-digit trailing date is a reliable signal.
20
64
  */
21
- const DEFAULT_CONTEXT_WINDOW = 200_000;
65
+ function stripAnthropicDateSuffix(model: string): string {
66
+ return model.replace(/-(\d{8})$/, "");
67
+ }
22
68
 
23
69
  export function getContextWindowSize(model: string): number {
24
- return CONTEXT_WINDOW_DEFAULTS[model] ?? DEFAULT_CONTEXT_WINDOW;
70
+ // Fast path: exact match (shortname or family-versioned id).
71
+ if (CONTEXT_WINDOW_DEFAULTS[model] !== undefined) {
72
+ return CONTEXT_WINDOW_DEFAULTS[model];
73
+ }
74
+ // Dated full id → strip suffix and retry.
75
+ const stripped = stripAnthropicDateSuffix(model);
76
+ if (stripped !== model && CONTEXT_WINDOW_DEFAULTS[stripped] !== undefined) {
77
+ return CONTEXT_WINDOW_DEFAULTS[stripped];
78
+ }
79
+ // OpenAI / GPT family — most reasoning models have 200k+; we keep this
80
+ // conservative and let callers override via models.dev rates if they want.
81
+ // Specific gpt-5.x context windows are >1M but the local-CLI adapter
82
+ // generally doesn't surface those; the API recompute path uses the rate
83
+ // table, not the window. The 200k default keeps the math safe.
84
+ return DEFAULT_CONTEXT_WINDOW;
25
85
  }
26
86
 
27
87
  /**
28
88
  * Compute the total context tokens used from a Claude API usage object.
29
89
  * Sums input_tokens + cache_creation_input_tokens + cache_read_input_tokens.
90
+ *
91
+ * @deprecated Phase 9 — use {@link computeContextUsedUnified} instead. This
92
+ * variant excludes output tokens, which is the wrong number when the goal is
93
+ * "how full is the model's context window right now."
30
94
  */
31
95
  export function computeContextUsed(usage: {
32
96
  input_tokens?: number | null;
@@ -39,3 +103,39 @@ export function computeContextUsed(usage: {
39
103
  (usage.cache_read_input_tokens ?? 0)
40
104
  );
41
105
  }
106
+
107
+ /**
108
+ * Phase 9: the unified context-used formula adapters should use when emitting
109
+ * `context_usage` events. Sums input + cache_read + cache_create + output,
110
+ * which is the number the Claude Code status line shows. Cross-provider
111
+ * comparisons (claude vs codex vs pi) are only meaningful when every adapter
112
+ * agrees on this formula.
113
+ *
114
+ * Returns 0 if every field is missing; callers should check the `contextTotal`
115
+ * separately and emit `null` for `contextPercent` when the window is unknown.
116
+ */
117
+ export function computeContextUsedUnified(parts: {
118
+ inputTokens?: number | null;
119
+ cacheReadTokens?: number | null;
120
+ cacheCreateTokens?: number | null;
121
+ outputTokens?: number | null;
122
+ }): number {
123
+ return (
124
+ (parts.inputTokens ?? 0) +
125
+ (parts.cacheReadTokens ?? 0) +
126
+ (parts.cacheCreateTokens ?? 0) +
127
+ (parts.outputTokens ?? 0)
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Phase 9: clamp a raw context-percent value to [0, 100]. Returns null when
133
+ * `total` is missing or 0 so callers can show "unknown" instead of a
134
+ * divide-by-zero NaN/∞.
135
+ */
136
+ export function clampContextPercent(used: number, total: number | null | undefined): number | null {
137
+ if (!total || total <= 0) return null;
138
+ const raw = (used / total) * 100;
139
+ if (!Number.isFinite(raw)) return null;
140
+ return Math.min(100, Math.max(0, raw));
141
+ }
@@ -6,14 +6,16 @@
6
6
  *
7
7
  * Cookie payload: `{pageId, exp}` where `exp` is a unix seconds timestamp.
8
8
  * Wire shape: `${base64url(JSON.stringify(payload))}.${base64url(HMAC-SHA256(payload, secret))}`.
9
- * Secret resolution: `process.env.PAGE_SESSION_SECRET || process.env.API_KEY`
10
- * — the API_KEY fallback keeps existing dev setups working without forcing a
11
- * new env var. Verification is constant-time via `crypto.timingSafeEqual` so
12
- * we don't leak bits via signature-comparison timing.
9
+ * Secret resolution: `process.env.PAGE_SESSION_SECRET || getApiKey()`
10
+ * — the swarm API-key fallback keeps existing dev setups working without
11
+ * forcing a new env var. Verification is constant-time via
12
+ * `crypto.timingSafeEqual` so we don't leak bits via signature-comparison
13
+ * timing.
13
14
  *
14
15
  * Both functions are async because `crypto.subtle.sign` is async.
15
16
  */
16
17
  import { timingSafeEqual } from "node:crypto";
18
+ import { getApiKey } from "./api-key";
17
19
 
18
20
  export interface PageSessionPayload {
19
21
  pageId: string;
@@ -43,12 +45,12 @@ function base64urlDecode(input: string): Uint8Array {
43
45
 
44
46
  /** Resolve the HMAC secret. */
45
47
  function getSecret(): string {
46
- const secret = process.env.PAGE_SESSION_SECRET || process.env.API_KEY;
48
+ const secret = process.env.PAGE_SESSION_SECRET || getApiKey();
47
49
  if (!secret) {
48
50
  // Fail-closed: better to refuse to issue cookies than to mint with an
49
51
  // empty key (any attacker who learns the implementation can forge).
50
52
  throw new Error(
51
- "page-session: neither PAGE_SESSION_SECRET nor API_KEY is set; refusing to sign/verify",
53
+ "page-session: neither PAGE_SESSION_SECRET nor swarm API key is set; refusing to sign/verify",
52
54
  );
53
55
  }
54
56
  return secret;
@@ -61,6 +61,8 @@ const SENSITIVE_KEY_EXACT = new Set<string>([
61
61
  "GSC_SERVICE_ACCOUNT_BASE64",
62
62
  "LINEAR_API_KEY",
63
63
  "LINEAR_OAUTH_CLIENT_SECRET",
64
+ "OTEL_EXPORTER_OTLP_HEADERS",
65
+ "SIGNOZ_INGESTION_KEY",
64
66
  ]);
65
67
 
66
68
  /** Suffixes that mark an env-var value as sensitive by convention. */
@@ -117,6 +119,11 @@ const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
117
119
  name: "jwt",
118
120
  re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
119
121
  },
122
+ // SigNoz Cloud OTLP auth header values.
123
+ {
124
+ name: "signoz_ingestion_key",
125
+ re: /\bsignoz-ingestion-key=[A-Za-z0-9._~+/-]{20,}={0,2}\b/g,
126
+ },
120
127
  ];
121
128
 
122
129
  interface EnvValueEntry {
@@ -143,7 +150,7 @@ function snapshotEnv(): string {
143
150
  return parts.join("|");
144
151
  }
145
152
 
146
- function isSensitiveKey(key: string): boolean {
153
+ export function isSensitiveKey(key: string): boolean {
147
154
  if (NON_SECRET_EXCEPTIONS.has(key)) return false;
148
155
  if (SENSITIVE_KEY_EXACT.has(key)) return true;
149
156
  for (const suffix of SENSITIVE_KEY_SUFFIXES) {
@@ -220,6 +227,27 @@ export function scrubSecrets(text: string | null | undefined): string {
220
227
  return out;
221
228
  }
222
229
 
230
+ export function scrubObject<T>(value: T, seen = new WeakSet<object>()): T {
231
+ if (value === null || value === undefined) return value;
232
+ if (typeof value === "string") return scrubSecrets(value) as T;
233
+ if (typeof value !== "object") return value;
234
+
235
+ if (seen.has(value)) {
236
+ return "[Circular]" as T;
237
+ }
238
+ seen.add(value);
239
+
240
+ if (Array.isArray(value)) {
241
+ return value.map((item) => scrubObject(item, seen)) as T;
242
+ }
243
+
244
+ const out: Record<string, unknown> = {};
245
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
246
+ out[key] = scrubObject(child, seen);
247
+ }
248
+ return out as T;
249
+ }
250
+
223
251
  /**
224
252
  * Force the env-value cache to rebuild on the next scrub call. Callers should
225
253
  * invoke this whenever the swarm_config is reloaded (`/internal/reload-config`
@@ -16,7 +16,7 @@ import { shouldSkipCooldown } from "./cooldown";
16
16
  import { findEntryNodes, getNextTargets, getSuccessors } from "./definition";
17
17
  import type { AsyncExecutorResult } from "./executors/base";
18
18
  import type { ExecutorRegistry } from "./executors/registry";
19
- import { resolveInputs } from "./input";
19
+ import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
20
20
  import { validateJsonSchema } from "./json-schema-validator";
21
21
  import { deepInterpolate } from "./template";
22
22
  import { runStepValidation, type ValidationRunResult } from "./validation";
@@ -97,7 +97,8 @@ export async function startWorkflowExecution(
97
97
  }
98
98
 
99
99
  const entryNodes = findEntryNodes(workflow.definition);
100
- await walkGraph(workflow.definition, runId, ctx, entryNodes, registry, workflow.id);
100
+ const secretKeys = getSecretInputKeys(workflow.input);
101
+ await walkGraph(workflow.definition, runId, ctx, entryNodes, registry, workflow.id, secretKeys);
101
102
  return runId;
102
103
  }
103
104
 
@@ -125,6 +126,7 @@ export async function walkGraph(
125
126
  startNodes: WorkflowNode[],
126
127
  registry: ExecutorRegistry,
127
128
  workflowId?: string,
129
+ secretKeys: Set<string> = new Set(),
128
130
  ): Promise<void> {
129
131
  let nodeExecutionCount = 0;
130
132
  const completedNodeIds = new Set(getCompletedStepNodeIds(runId));
@@ -232,7 +234,7 @@ export async function walkGraph(
232
234
  // Execute all pending nodes in parallel
233
235
  const results = await Promise.all(
234
236
  pendingNodes.map((node) =>
235
- executeStep(def, runId, ctx, node, registry, workflowId).catch(
237
+ executeStep(def, runId, ctx, node, registry, workflowId, secretKeys).catch(
236
238
  (_err): StepResult => ({
237
239
  outcome: "failed",
238
240
  successors: [],
@@ -382,6 +384,7 @@ async function executeStep(
382
384
  node: WorkflowNode,
383
385
  registry: ExecutorRegistry,
384
386
  workflowId?: string,
387
+ secretKeys: Set<string> = new Set(),
385
388
  ): Promise<StepResult> {
386
389
  // Use iteration-aware idempotency key to support loops.
387
390
  // Count existing steps for this node to determine the current iteration.
@@ -407,13 +410,18 @@ async function executeStep(
407
410
  }
408
411
 
409
412
  // 2. Create step
413
+ // Redact resolved secret values from the persisted input so credentials
414
+ // stored in `ctx.input` (resolved via `secret.*` or sensitive `${ENV}`
415
+ // references) don't leak through `get-workflow-run` or any other reader of
416
+ // the `workflow_run_steps` table. The live `ctx` is untouched — executors
417
+ // still see real values.
410
418
  const stepId = crypto.randomUUID();
411
419
  createWorkflowRunStep({
412
420
  id: stepId,
413
421
  runId,
414
422
  nodeId: node.id,
415
423
  nodeType: node.type,
416
- input: ctx,
424
+ input: redactSecretsForStorage(ctx, secretKeys),
417
425
  });
418
426
 
419
427
  // Set idempotency key
@@ -12,6 +12,7 @@ export { PropertyMatchExecutor } from "./property-match";
12
12
  export { RawLlmExecutor } from "./raw-llm";
13
13
  export { createExecutorRegistry, ExecutorRegistry } from "./registry";
14
14
  export { ScriptExecutor } from "./script";
15
+ export { SwarmScriptExecutor } from "./swarm-script";
15
16
  export { ValidateExecutor } from "./validate";
16
17
  export { VcsExecutor } from "./vcs";
17
18
  export { WaitExecutor } from "./wait";