@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.
- package/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
package/src/tools/tool-config.ts
CHANGED
|
@@ -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",
|
package/src/tools/utils.ts
CHANGED
|
@@ -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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
595
|
-
//
|
|
596
|
-
//
|
|
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(
|
|
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
|
-
|
|
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([
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
65
|
+
function stripAnthropicDateSuffix(model: string): string {
|
|
66
|
+
return model.replace(/-(\d{8})$/, "");
|
|
67
|
+
}
|
|
22
68
|
|
|
23
69
|
export function getContextWindowSize(model: string): number {
|
|
24
|
-
|
|
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 ||
|
|
10
|
-
* — the
|
|
11
|
-
* new env var. Verification is constant-time via
|
|
12
|
-
* we don't leak bits via signature-comparison
|
|
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 ||
|
|
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
|
|
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`
|
package/src/workflows/engine.ts
CHANGED
|
@@ -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
|
-
|
|
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";
|