@stigmer/runner 3.5.3 → 3.7.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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-agent.js +85 -10
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/discover-mcp-server.js +9 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -0
- package/dist/activities/execute-cursor/index.js +88 -14
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
- package/dist/activities/execute-cursor/model-pricing.js +19 -0
- package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -0
- package/dist/activities/execute-cursor/prompt-builder.js +9 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
- package/dist/activities/execute-cursor/service-tier.js +187 -0
- package/dist/activities/execute-cursor/service-tier.js.map +1 -0
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
- package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
- package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
- package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +6 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +45 -9
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +32 -1
- package/dist/client/stigmer-client.js +42 -2
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -1
- package/dist/runner.js +48 -0
- package/dist/runner.js.map +1 -1
- package/dist/sandbox-token-renewal.d.ts +65 -0
- package/dist/sandbox-token-renewal.js +169 -0
- package/dist/sandbox-token-renewal.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +17 -3
- package/dist/shared/artifact-storage.js +22 -4
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +89 -0
- package/dist/shared/caller-identity.js +124 -0
- package/dist/shared/caller-identity.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +85 -0
- package/dist/shared/channel-attachment.js +203 -0
- package/dist/shared/channel-attachment.js.map +1 -0
- package/dist/shared/datastore-attachment.d.ts +2 -25
- package/dist/shared/datastore-attachment.js +1 -28
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +51 -0
- package/dist/shared/synthesized-attachment.js +45 -0
- package/dist/shared/synthesized-attachment.js.map +1 -0
- package/dist/workflow-engine/loader.js +99 -2
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
- package/dist/workflow-engine/tasks/call-agent.js +0 -2
- package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +39 -7
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
- package/dist/workflows/call-agent-orchestrator.js +8 -2
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-utils__/mock-client.ts +4 -0
- package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
- package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
- package/src/activities/__tests__/call-agent.test.ts +219 -4
- package/src/activities/__tests__/discover-mcp-server.test.ts +49 -0
- package/src/activities/call-agent.ts +94 -10
- package/src/activities/discover-mcp-server.ts +13 -1
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
- package/src/activities/execute-cursor/index.ts +121 -20
- package/src/activities/execute-cursor/model-pricing.ts +23 -0
- package/src/activities/execute-cursor/prompt-builder.ts +19 -0
- package/src/activities/execute-cursor/service-tier.ts +244 -0
- package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
- package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +10 -0
- package/src/activities/execute-deep-agent/setup.ts +66 -10
- package/src/client/stigmer-client.ts +57 -4
- package/src/main.ts +20 -0
- package/src/runner.ts +62 -0
- package/src/sandbox-token-renewal.ts +212 -0
- package/src/shared/__tests__/caller-identity.test.ts +159 -0
- package/src/shared/__tests__/channel-attachment.test.ts +276 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +4 -4
- package/src/shared/artifact-storage.ts +32 -7
- package/src/shared/caller-identity.ts +161 -0
- package/src/shared/channel-attachment.ts +237 -0
- package/src/shared/datastore-attachment.ts +2 -54
- package/src/shared/synthesized-attachment.ts +77 -0
- package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
- package/src/workflow-engine/__tests__/loader.test.ts +192 -7
- package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
- package/src/workflow-engine/loader.ts +113 -2
- package/src/workflow-engine/tasks/call-agent.ts +0 -2
- package/src/workflow-engine/types.ts +40 -7
- package/src/workflows/call-agent-orchestrator.ts +8 -2
|
@@ -411,6 +411,55 @@ describe("DiscoverMcpServer activity", () => {
|
|
|
411
411
|
expect(mockClient.getExecutionContextByExecutionId).toHaveBeenCalledWith("ctx-abc");
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
+
it("discovers a caller-identity-templating server via the anonymous sentinel", async () => {
|
|
415
|
+
// Discovery has no session, so declared STIGMER_CALLER_IDENTITY_*
|
|
416
|
+
// placeholders resolve to the anonymous sentinel instead of failing
|
|
417
|
+
// with PlaceholderResolutionError — the failure mode that would
|
|
418
|
+
// leave an identity-consuming server's tools permanently
|
|
419
|
+
// unclassified. The server sees an anonymous caller and must answer
|
|
420
|
+
// tools/list (its authz layer refuses tool CALLS, not discovery).
|
|
421
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
422
|
+
const { MultiServerMCPClient } = await import("@langchain/mcp-adapters");
|
|
423
|
+
|
|
424
|
+
const spec = makeHttpSpec("https://isc-mcp.example.com/mcp");
|
|
425
|
+
spec.serverType.value.headers = {
|
|
426
|
+
"X-Stigmer-Caller-Kind": "${STIGMER_CALLER_IDENTITY_KIND}",
|
|
427
|
+
"X-Stigmer-Caller-Value": "${STIGMER_CALLER_IDENTITY_VALUE}",
|
|
428
|
+
Authorization: "Bearer ${ISC_SHARED_SECRET}",
|
|
429
|
+
};
|
|
430
|
+
spec.env = {
|
|
431
|
+
STIGMER_CALLER_IDENTITY_KIND: { optional: true },
|
|
432
|
+
STIGMER_CALLER_IDENTITY_VALUE: { optional: true },
|
|
433
|
+
ISC_SHARED_SECRET: { isSecret: true },
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const mockClient = makeMockStigmerClient({
|
|
437
|
+
mcpServer: makeMcpServer({ metadata: { slug: "isc-gym" }, spec }),
|
|
438
|
+
executionContext: {
|
|
439
|
+
spec: { data: { ISC_SHARED_SECRET: { value: "s3cret", isSecret: true } } },
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const mockMcpClient = makeMockMcpClient({
|
|
444
|
+
tools: [{ name: "get_gym_info", description: "Public info", inputSchema: { type: "object" } }],
|
|
445
|
+
});
|
|
446
|
+
mockInitializeConnections.mockResolvedValue({});
|
|
447
|
+
mockGetClient.mockResolvedValue(mockMcpClient);
|
|
448
|
+
|
|
449
|
+
const result = await discoverMcpServer(
|
|
450
|
+
{ mcpServerId: "mcp-isc", executionContextId: "ctx-isc" },
|
|
451
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(result.tools).toHaveLength(1);
|
|
455
|
+
const connectionConfig = vi.mocked(MultiServerMCPClient).mock.calls[0][0] as any;
|
|
456
|
+
expect(connectionConfig["isc-gym"].headers).toEqual({
|
|
457
|
+
"X-Stigmer-Caller-Kind": "anonymous",
|
|
458
|
+
"X-Stigmer-Caller-Value": "",
|
|
459
|
+
Authorization: "Bearer s3cret",
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
414
463
|
it("returns empty env when no executionContextId provided", async () => {
|
|
415
464
|
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
416
465
|
|
|
@@ -31,6 +31,13 @@ import { AgentExecutionSpecSchema, ExecutionConfigSchema } from "@stigmer/protos
|
|
|
31
31
|
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
32
32
|
import { SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
|
|
33
33
|
import { Harness, ExecutionTarget } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
34
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
35
|
+
import {
|
|
36
|
+
WorkspaceEntrySchema,
|
|
37
|
+
WorkspaceSourceSchema,
|
|
38
|
+
GitRepoSourceSchema,
|
|
39
|
+
type WorkspaceEntry,
|
|
40
|
+
} from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
34
41
|
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
35
42
|
import { ExecutionValueSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
36
43
|
import type { ExecutionValue } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
@@ -62,14 +69,15 @@ export async function callAgentAction(
|
|
|
62
69
|
);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
// The execution is always created in the workflow's org — the workflow
|
|
73
|
+
// owner pays for the run. A cross-org agent reference ("org/slug") only
|
|
74
|
+
// changes where the agent blueprint is looked up, never the billing org.
|
|
75
|
+
const orgId = (runtimeEnv["__stigmer_org_id"] as string | undefined) ?? "";
|
|
68
76
|
|
|
69
77
|
if (!orgId) {
|
|
70
78
|
throw new Error(
|
|
71
79
|
"call:agent requires an organization context. " +
|
|
72
|
-
"
|
|
80
|
+
"Ensure '__stigmer_org_id' is in the workflow environment.",
|
|
73
81
|
);
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -142,6 +150,7 @@ export async function callAgentAction(
|
|
|
142
150
|
harness,
|
|
143
151
|
executionTarget,
|
|
144
152
|
subject: "Auto-created session",
|
|
153
|
+
workspaceEntries: buildWorkspaceEntries(resolved.workspace_entries),
|
|
145
154
|
}),
|
|
146
155
|
}),
|
|
147
156
|
);
|
|
@@ -170,10 +179,17 @@ export async function callAgentAction(
|
|
|
170
179
|
);
|
|
171
180
|
}
|
|
172
181
|
|
|
173
|
-
// Task-config-level env takes precedence over auto-forwarded values
|
|
182
|
+
// Task-config-level env takes precedence over auto-forwarded values.
|
|
183
|
+
// The agent's declared secret marking survives the override: a key the
|
|
184
|
+
// agent declares secret stays secret no matter which channel supplied
|
|
185
|
+
// the value, so an explicit task-level `env:` entry cannot downgrade
|
|
186
|
+
// redaction (issue #358 — the override used to hardcode isSecret:false).
|
|
174
187
|
if (resolved.env) {
|
|
175
188
|
for (const [key, value] of Object.entries(resolved.env)) {
|
|
176
|
-
executionRuntimeEnv[key] = {
|
|
189
|
+
executionRuntimeEnv[key] = {
|
|
190
|
+
value: String(value),
|
|
191
|
+
isSecret: agentEnvDecls[key]?.isSecret ?? false,
|
|
192
|
+
};
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
|
|
@@ -196,13 +212,41 @@ export async function callAgentAction(
|
|
|
196
212
|
const parentQueue = runtimeEnv["__stigmer_activity_task_queue"] as string | undefined;
|
|
197
213
|
const activityTaskQueue = parentQueue?.startsWith("wfexec:") ? parentQueue : "";
|
|
198
214
|
|
|
199
|
-
|
|
215
|
+
// Honest RunConfig → ExecutionConfig mapping (issue #358): every field
|
|
216
|
+
// the author may set is forwarded to a field the runner enforces.
|
|
217
|
+
// model_name replaces the agent's default outright; max_cost_usd feeds
|
|
218
|
+
// the harness-generic cost guards (cost-cap middleware / cursor
|
|
219
|
+
// cost-guard); max_tool_rounds feeds resolveRecursionLimit (native
|
|
220
|
+
// harness only); service_tier feeds the cursor harness's explicit
|
|
221
|
+
// variant selection (issue #357). Zero/unset means "no override" and is
|
|
222
|
+
// omitted.
|
|
223
|
+
const runConfig = resolved.run_config;
|
|
224
|
+
const hasModel = !!runConfig?.model_name;
|
|
225
|
+
const hasCostCap = (runConfig?.max_cost_usd ?? 0) > 0;
|
|
226
|
+
const hasToolRounds = (runConfig?.max_tool_rounds ?? 0) > 0;
|
|
227
|
+
// Loader guarantees a canonical enum name; an unknown one here means the
|
|
228
|
+
// loader and this mapping drifted — fail the task, never silently drop a
|
|
229
|
+
// pricing directive.
|
|
230
|
+
const SERVICE_TIER_BY_NAME: Record<string, ServiceTier> = {
|
|
231
|
+
SERVICE_TIER_STANDARD: ServiceTier.STANDARD,
|
|
232
|
+
SERVICE_TIER_FAST: ServiceTier.FAST,
|
|
233
|
+
};
|
|
234
|
+
const serviceTier = runConfig?.service_tier
|
|
235
|
+
? SERVICE_TIER_BY_NAME[runConfig.service_tier]
|
|
236
|
+
: undefined;
|
|
237
|
+
if (runConfig?.service_tier && serviceTier === undefined) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`call:agent run_config.service_tier '${runConfig.service_tier}' has no proto mapping`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const hasServiceTier = serviceTier !== undefined;
|
|
200
243
|
const hasOutputSchema = !!resolved.output?.schema;
|
|
201
244
|
|
|
202
245
|
console.log(
|
|
203
246
|
`[CallAgent] schema propagation diagnostic: ` +
|
|
204
247
|
`hasOutputSchema=${hasOutputSchema}, ` +
|
|
205
|
-
`hasModel=${hasModel}, ` +
|
|
248
|
+
`hasModel=${hasModel}, hasCostCap=${hasCostCap}, hasToolRounds=${hasToolRounds}, ` +
|
|
249
|
+
`hasServiceTier=${hasServiceTier}, ` +
|
|
206
250
|
`configKeys=[${Object.keys(resolved).join(",")}], ` +
|
|
207
251
|
`hasOutput=${resolved.output !== undefined}, ` +
|
|
208
252
|
`outputKeys=${resolved.output ? JSON.stringify(Object.keys(resolved.output)) : "N/A"}, ` +
|
|
@@ -220,15 +264,29 @@ export async function callAgentAction(
|
|
|
220
264
|
runtimeEnv: runtimeEnvProto,
|
|
221
265
|
});
|
|
222
266
|
|
|
223
|
-
if (hasModel || hasOutputSchema) {
|
|
267
|
+
if (hasModel || hasCostCap || hasToolRounds || hasServiceTier || hasOutputSchema) {
|
|
224
268
|
const execConfig = create(ExecutionConfigSchema, {});
|
|
225
|
-
if (hasModel) execConfig.modelName =
|
|
269
|
+
if (hasModel) execConfig.modelName = runConfig!.model_name!;
|
|
270
|
+
if (hasCostCap) execConfig.maxCostUsd = runConfig!.max_cost_usd!;
|
|
271
|
+
if (hasToolRounds) execConfig.maxToolRounds = runConfig!.max_tool_rounds!;
|
|
272
|
+
if (hasServiceTier) execConfig.serviceTier = serviceTier!;
|
|
226
273
|
if (hasOutputSchema) {
|
|
227
274
|
execConfig.structuredOutputSchema = resolved.output!.schema as JsonObject;
|
|
228
275
|
}
|
|
229
276
|
executionSpec.executionConfig = execConfig;
|
|
230
277
|
}
|
|
231
278
|
|
|
279
|
+
// Workflow provenance labels: the server's CreateExecutionContextStep
|
|
280
|
+
// keys the agent_call environment_refs resolution on these (the
|
|
281
|
+
// schedule-label lineage). The cloud edition additionally gates the
|
|
282
|
+
// branch on the trusted runner caller identity, so the labels are only
|
|
283
|
+
// load-bearing inside that trust boundary.
|
|
284
|
+
const labels: Record<string, string> = {};
|
|
285
|
+
if (wfExecId && taskName) {
|
|
286
|
+
labels["stigmer.ai/workflow-execution-id"] = wfExecId;
|
|
287
|
+
labels["stigmer.ai/workflow-task"] = taskName;
|
|
288
|
+
}
|
|
289
|
+
|
|
232
290
|
await client.createAgentExecution(
|
|
233
291
|
create(AgentExecutionSchema, {
|
|
234
292
|
apiVersion: "agentic.stigmer.ai/v1",
|
|
@@ -236,6 +294,7 @@ export async function callAgentAction(
|
|
|
236
294
|
metadata: create(ApiResourceMetadataSchema, {
|
|
237
295
|
name: executionName,
|
|
238
296
|
org: orgId,
|
|
297
|
+
labels,
|
|
239
298
|
}),
|
|
240
299
|
spec: executionSpec,
|
|
241
300
|
}),
|
|
@@ -244,6 +303,31 @@ export async function callAgentAction(
|
|
|
244
303
|
throw new CompleteAsyncError();
|
|
245
304
|
}
|
|
246
305
|
|
|
306
|
+
// buildWorkspaceEntries maps the task's git-only workspace entries onto the
|
|
307
|
+
// shared session WorkspaceEntry proto. Provisioning credentials are NOT the
|
|
308
|
+
// runner's concern here: the provisioner resolves GITHUB_TOKEN from the
|
|
309
|
+
// merged environment (DD-018 D-4), which the task's environment_refs feed
|
|
310
|
+
// via server-side resolution.
|
|
311
|
+
function buildWorkspaceEntries(
|
|
312
|
+
entries: AgentCallConfig["workspace_entries"],
|
|
313
|
+
): WorkspaceEntry[] {
|
|
314
|
+
if (!entries || entries.length === 0) return [];
|
|
315
|
+
return entries.map((entry) =>
|
|
316
|
+
create(WorkspaceEntrySchema, {
|
|
317
|
+
name: entry.name ?? "",
|
|
318
|
+
source: create(WorkspaceSourceSchema, {
|
|
319
|
+
source: {
|
|
320
|
+
case: "gitRepo",
|
|
321
|
+
value: create(GitRepoSourceSchema, {
|
|
322
|
+
url: entry.source.git_repo.url,
|
|
323
|
+
branch: entry.source.git_repo.branch ?? "",
|
|
324
|
+
}),
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
247
331
|
function parseAgentReference(
|
|
248
332
|
agentStr: string,
|
|
249
333
|
defaultOrg: string,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type McpTransportPosture,
|
|
31
31
|
} from "../shared/mcp-transport-guard.js";
|
|
32
32
|
import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
|
|
33
|
+
import { injectAnonymousCallerIdentityForDiscovery } from "../shared/caller-identity.js";
|
|
33
34
|
import { withTimeout } from "../shared/with-timeout.js";
|
|
34
35
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
35
36
|
import type { Config } from "../config.js";
|
|
@@ -249,7 +250,18 @@ export async function discoverMcpServer(
|
|
|
249
250
|
const declaredEnvKeys = mcpServer.spec.env
|
|
250
251
|
? new Set(Object.keys(mcpServer.spec.env))
|
|
251
252
|
: new Set<string>();
|
|
252
|
-
const
|
|
253
|
+
const platformEnv = injectPlatformEnv(declaredEnvKeys, envVars);
|
|
254
|
+
|
|
255
|
+
// Discovery runs with no session, so every declared caller-identity key
|
|
256
|
+
// resolves to the anonymous sentinel — without it, a server templating
|
|
257
|
+
// ${STIGMER_CALLER_IDENTITY_VALUE} in its headers would fail discovery
|
|
258
|
+
// with PlaceholderResolutionError and its tools would never be
|
|
259
|
+
// classified. Servers consuming these keys answer tools/list to
|
|
260
|
+
// anonymous callers by contract.
|
|
261
|
+
const finalEnv = injectAnonymousCallerIdentityForDiscovery(
|
|
262
|
+
declaredEnvKeys,
|
|
263
|
+
platformEnv,
|
|
264
|
+
);
|
|
253
265
|
|
|
254
266
|
const resolved = mcpServerToResolved(mcpServer, slug, finalEnv);
|
|
255
267
|
if (!resolved) {
|
|
@@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
|
7
7
|
*/
|
|
8
8
|
describe("getCursorModelPricing — speed variant resolution", () => {
|
|
9
9
|
let getCursorModelPricing: typeof import("../model-pricing.js").getCursorModelPricing;
|
|
10
|
+
let getCursorModelPricingForVariant: typeof import("../model-pricing.js").getCursorModelPricingForVariant;
|
|
10
11
|
let computeTurnCost: typeof import("../model-pricing.js").computeTurnCost;
|
|
11
12
|
|
|
12
13
|
beforeAll(async () => {
|
|
@@ -58,6 +59,7 @@ describe("getCursorModelPricing — speed variant resolution", () => {
|
|
|
58
59
|
const mod = await import("../model-pricing.js");
|
|
59
60
|
await mod.ensureLoaded();
|
|
60
61
|
getCursorModelPricing = mod.getCursorModelPricing;
|
|
62
|
+
getCursorModelPricingForVariant = mod.getCursorModelPricingForVariant;
|
|
61
63
|
computeTurnCost = mod.computeTurnCost;
|
|
62
64
|
});
|
|
63
65
|
|
|
@@ -89,4 +91,22 @@ describe("getCursorModelPricing — speed variant resolution", () => {
|
|
|
89
91
|
const cost = computeTurnCost(p, 266_945, 7_069, 0, 222_432);
|
|
90
92
|
expect(cost).toBeCloseTo(0.28406, 5);
|
|
91
93
|
});
|
|
94
|
+
|
|
95
|
+
it("getCursorModelPricingForVariant('fast') prices a base id at fast rates (#357)", () => {
|
|
96
|
+
// The explicit-tier path: the caller KNOWS the variant (it requested
|
|
97
|
+
// it) — no wire-id suffix inference involved.
|
|
98
|
+
const p = getCursorModelPricingForVariant("composer-2.5", "fast");
|
|
99
|
+
expect(p.inputPricePerMillion).toBe(3.0);
|
|
100
|
+
expect(p.outputPricePerMillion).toBe(15.0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("getCursorModelPricingForVariant(null) keeps base rates", () => {
|
|
104
|
+
const p = getCursorModelPricingForVariant("composer-2.5", null);
|
|
105
|
+
expect(p.inputPricePerMillion).toBe(0.5);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("getCursorModelPricingForVariant('fast') falls back to base rates when unpriced", () => {
|
|
109
|
+
const p = getCursorModelPricingForVariant("claude-opus-4-6", "fast");
|
|
110
|
+
expect(p.inputPricePerMillion).toBe(5.0);
|
|
111
|
+
});
|
|
92
112
|
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verifies the service-tier → Cursor variant-parameter translation (#357):
|
|
6
|
+
* the runner must always send an explicit selection whose price-bearing
|
|
7
|
+
* parameters are a deterministic function of the requested tier — never
|
|
8
|
+
* the catalog's (account-influenced) default variant.
|
|
9
|
+
*
|
|
10
|
+
* Catalog fixtures mirror the real shapes observed 2026-08-06: composer
|
|
11
|
+
* has a `fast` bool (default fast=true), haiku has a `thinking` bool
|
|
12
|
+
* (default thinking=true), grok has `effort` x `fast`, and Auto
|
|
13
|
+
* ("default") has a single variant with no parameters.
|
|
14
|
+
*/
|
|
15
|
+
const listMock = vi.hoisted(() => vi.fn());
|
|
16
|
+
|
|
17
|
+
vi.mock("@cursor/sdk", () => ({
|
|
18
|
+
Cursor: { models: { list: listMock } },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
resolveEffectiveServiceTier,
|
|
23
|
+
resolveServiceTierParams,
|
|
24
|
+
resetCatalogCacheForTests,
|
|
25
|
+
} from "../service-tier.js";
|
|
26
|
+
|
|
27
|
+
const CATALOG = [
|
|
28
|
+
{
|
|
29
|
+
id: "composer-2.5",
|
|
30
|
+
displayName: "Composer 2.5",
|
|
31
|
+
aliases: ["composer-latest", "composer"],
|
|
32
|
+
parameters: [
|
|
33
|
+
{ id: "fast", values: [{ value: "false" }, { value: "true" }] },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "claude-haiku-4-5",
|
|
38
|
+
displayName: "Haiku 4.5",
|
|
39
|
+
aliases: ["haiku"],
|
|
40
|
+
parameters: [
|
|
41
|
+
{ id: "thinking", values: [{ value: "false" }, { value: "true" }] },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "grok-4.5",
|
|
46
|
+
displayName: "Cursor Grok 4.5",
|
|
47
|
+
parameters: [
|
|
48
|
+
{ id: "effort", values: [{ value: "low" }, { value: "medium" }, { value: "high" }] },
|
|
49
|
+
{ id: "fast", values: [{ value: "false" }, { value: "true" }] },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "claude-opus-4-8",
|
|
54
|
+
displayName: "Opus 4.8",
|
|
55
|
+
parameters: [
|
|
56
|
+
{ id: "thinking", values: [{ value: "false" }, { value: "true" }] },
|
|
57
|
+
{ id: "effort", values: [{ value: "low" }, { value: "high" }] },
|
|
58
|
+
{ id: "fast", values: [{ value: "false" }, { value: "true" }] },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "default",
|
|
63
|
+
displayName: "Auto",
|
|
64
|
+
aliases: ["auto"],
|
|
65
|
+
variants: [{ params: [], displayName: "Auto", isDefault: true }],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function opts(modelId: string, tier: ServiceTier.STANDARD | ServiceTier.FAST) {
|
|
70
|
+
return { apiKey: "key-1", modelId, tier, executionId: "aex_test" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
resetCatalogCacheForTests();
|
|
75
|
+
listMock.mockReset();
|
|
76
|
+
listMock.mockResolvedValue(CATALOG);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("resolveEffectiveServiceTier", () => {
|
|
80
|
+
it("resolves UNSPECIFIED to STANDARD — never the account default", () => {
|
|
81
|
+
expect(resolveEffectiveServiceTier(ServiceTier.UNSPECIFIED)).toBe(ServiceTier.STANDARD);
|
|
82
|
+
expect(resolveEffectiveServiceTier(undefined)).toBe(ServiceTier.STANDARD);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("preserves explicit STANDARD and FAST", () => {
|
|
86
|
+
expect(resolveEffectiveServiceTier(ServiceTier.STANDARD)).toBe(ServiceTier.STANDARD);
|
|
87
|
+
expect(resolveEffectiveServiceTier(ServiceTier.FAST)).toBe(ServiceTier.FAST);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("resolveServiceTierParams", () => {
|
|
92
|
+
it("STANDARD pins fast=false on a fast-capable model", async () => {
|
|
93
|
+
const params = await resolveServiceTierParams(opts("composer-2.5", ServiceTier.STANDARD));
|
|
94
|
+
expect(params).toEqual([{ id: "fast", value: "false" }]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("FAST pins fast=true on a fast-capable model", async () => {
|
|
98
|
+
const params = await resolveServiceTierParams(opts("composer-2.5", ServiceTier.FAST));
|
|
99
|
+
expect(params).toEqual([{ id: "fast", value: "true" }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("STANDARD pins thinking=false on a thinking-capable model (the haiku drift)", async () => {
|
|
103
|
+
const params = await resolveServiceTierParams(opts("claude-haiku-4-5", ServiceTier.STANDARD));
|
|
104
|
+
expect(params).toEqual([{ id: "thinking", value: "false" }]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("FAST on a model with no fast parameter fails loudly, never downgrades", async () => {
|
|
108
|
+
await expect(
|
|
109
|
+
resolveServiceTierParams(opts("claude-haiku-4-5", ServiceTier.FAST)),
|
|
110
|
+
).rejects.toThrow(/no "fast" parameter/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("leaves price-neutral parameters (effort) to the catalog default", async () => {
|
|
114
|
+
const params = await resolveServiceTierParams(opts("grok-4.5", ServiceTier.STANDARD));
|
|
115
|
+
expect(params).toEqual([{ id: "fast", value: "false" }]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("pins every price-bearing parameter, sorted, on multi-dimension models", async () => {
|
|
119
|
+
const params = await resolveServiceTierParams(opts("claude-opus-4-8", ServiceTier.FAST));
|
|
120
|
+
expect(params).toEqual([
|
|
121
|
+
{ id: "fast", value: "true" },
|
|
122
|
+
{ id: "thinking", value: "false" },
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("resolves models referenced by alias", async () => {
|
|
127
|
+
const params = await resolveServiceTierParams(opts("composer", ServiceTier.STANDARD));
|
|
128
|
+
expect(params).toEqual([{ id: "fast", value: "false" }]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("Auto has no tier dimension: STANDARD sends no params", async () => {
|
|
132
|
+
const params = await resolveServiceTierParams(opts("default", ServiceTier.STANDARD));
|
|
133
|
+
expect(params).toEqual([]);
|
|
134
|
+
// No catalog fetch needed for Auto — nothing to look up.
|
|
135
|
+
expect(listMock).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("Auto + FAST is a loud failure (registry/catalog drift, not a silent no-op)", async () => {
|
|
139
|
+
await expect(
|
|
140
|
+
resolveServiceTierParams(opts("default", ServiceTier.FAST)),
|
|
141
|
+
).rejects.toThrow(/requires a pinned model/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("unknown model: STANDARD degrades to no params, FAST fails loudly", async () => {
|
|
145
|
+
await expect(
|
|
146
|
+
resolveServiceTierParams(opts("not-a-model", ServiceTier.STANDARD)),
|
|
147
|
+
).resolves.toEqual([]);
|
|
148
|
+
await expect(
|
|
149
|
+
resolveServiceTierParams(opts("not-a-model", ServiceTier.FAST)),
|
|
150
|
+
).rejects.toThrow(/does not list that model/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("catalog fetch failure: STANDARD degrades, FAST fails loudly", async () => {
|
|
154
|
+
listMock.mockRejectedValue(new Error("proxy down"));
|
|
155
|
+
await expect(
|
|
156
|
+
resolveServiceTierParams(opts("composer-2.5", ServiceTier.STANDARD)),
|
|
157
|
+
).resolves.toEqual([]);
|
|
158
|
+
resetCatalogCacheForTests();
|
|
159
|
+
listMock.mockRejectedValue(new Error("proxy down"));
|
|
160
|
+
await expect(
|
|
161
|
+
resolveServiceTierParams(opts("composer-2.5", ServiceTier.FAST)),
|
|
162
|
+
).rejects.toThrow(/catalog fetch failed/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("caches the catalog per worker — one fetch for repeated resolutions", async () => {
|
|
166
|
+
await resolveServiceTierParams(opts("composer-2.5", ServiceTier.STANDARD));
|
|
167
|
+
await resolveServiceTierParams(opts("claude-haiku-4-5", ServiceTier.STANDARD));
|
|
168
|
+
expect(listMock).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -1,7 +1,45 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
2
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
3
|
|
|
3
4
|
import { UsageAccumulator } from "../usage-accumulator.js";
|
|
4
5
|
|
|
6
|
+
// The accumulator's per-turn estimate reads the worker's pricing table
|
|
7
|
+
// (model-pricing.ts); load it from a stubbed registry so the fast-rate
|
|
8
|
+
// assertions run against known prices (the model-pricing.test.ts pattern).
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
const registry = {
|
|
11
|
+
models: [
|
|
12
|
+
{
|
|
13
|
+
id: "composer-2.5",
|
|
14
|
+
displayName: "Composer 2.5",
|
|
15
|
+
provider: "cursor",
|
|
16
|
+
harness: "cursor",
|
|
17
|
+
costTier: "economy",
|
|
18
|
+
pricing: {
|
|
19
|
+
inputPricePerMillion: 0.5,
|
|
20
|
+
outputPricePerMillion: 2.5,
|
|
21
|
+
cacheWritePricePerMillion: 0,
|
|
22
|
+
cacheReadPricePerMillion: 0.2,
|
|
23
|
+
},
|
|
24
|
+
pricingVariants: {
|
|
25
|
+
fast: {
|
|
26
|
+
inputPricePerMillion: 3.0,
|
|
27
|
+
outputPricePerMillion: 15.0,
|
|
28
|
+
cacheWritePricePerMillion: 0,
|
|
29
|
+
cacheReadPricePerMillion: 0.2,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
vi.stubGlobal(
|
|
36
|
+
"fetch",
|
|
37
|
+
vi.fn(async () => ({ ok: true, status: 200, json: async () => registry })),
|
|
38
|
+
);
|
|
39
|
+
process.env.STIGMER_TOKEN = "test-token";
|
|
40
|
+
await (await import("../model-pricing.js")).ensureLoaded();
|
|
41
|
+
});
|
|
42
|
+
|
|
5
43
|
/**
|
|
6
44
|
* Guards the token-accounting convention the Usage widget and the billing
|
|
7
45
|
* parity test both depend on: the Cursor SDK's inputTokens already INCLUDES
|
|
@@ -43,4 +81,52 @@ describe("UsageAccumulator", () => {
|
|
|
43
81
|
expect(snap.outputTokens).toBe(0n);
|
|
44
82
|
expect(snap.totalTokens).toBe(42n);
|
|
45
83
|
});
|
|
84
|
+
|
|
85
|
+
it("records the requested tier and params into the snapshot (#357 audit trail)", () => {
|
|
86
|
+
const acc = new UsageAccumulator(
|
|
87
|
+
"composer-2.5",
|
|
88
|
+
ServiceTier.FAST,
|
|
89
|
+
[{ id: "fast", value: "true" }],
|
|
90
|
+
);
|
|
91
|
+
acc.addTurn({ inputTokens: 10, outputTokens: 5 });
|
|
92
|
+
const snap = acc.snapshot();
|
|
93
|
+
expect(snap.requestedServiceTier).toBe(ServiceTier.FAST);
|
|
94
|
+
expect(snap.requestedModelParams).toBe('[{"id":"fast","value":"true"}]');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("records an empty params string when the runner sent none", () => {
|
|
98
|
+
const acc = new UsageAccumulator("default", ServiceTier.STANDARD, []);
|
|
99
|
+
acc.addTurn({ inputTokens: 1 });
|
|
100
|
+
const snap = acc.snapshot();
|
|
101
|
+
expect(snap.requestedServiceTier).toBe(ServiceTier.STANDARD);
|
|
102
|
+
expect(snap.requestedModelParams).toBe("");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("estimates FAST runs at fast-variant rates, not base rates (#357)", () => {
|
|
106
|
+
// Revert guard for the tier→pricing wiring in addTurn: a FAST run
|
|
107
|
+
// priced at base rates would understate the display estimate ~6x
|
|
108
|
+
// relative to the authoritative bill. Rates from the stubbed registry:
|
|
109
|
+
// base $0.5/$2.5 per M, fast $3/$15 per M.
|
|
110
|
+
const turn = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
|
|
111
|
+
|
|
112
|
+
const standard = new UsageAccumulator("composer-2.5", ServiceTier.STANDARD);
|
|
113
|
+
standard.addTurn(turn);
|
|
114
|
+
const fast = new UsageAccumulator("composer-2.5", ServiceTier.FAST);
|
|
115
|
+
fast.addTurn(turn);
|
|
116
|
+
|
|
117
|
+
expect(standard.snapshot().estimatedCostUsd).toBeCloseTo(3.0, 6);
|
|
118
|
+
expect(fast.snapshot().estimatedCostUsd).toBeCloseTo(18.0, 6);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("estimates UNSPECIFIED at base rates (resolves to standard)", () => {
|
|
122
|
+
const turn = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
|
|
123
|
+
|
|
124
|
+
const unspecified = new UsageAccumulator("composer-2.5");
|
|
125
|
+
unspecified.addTurn(turn);
|
|
126
|
+
const standard = new UsageAccumulator("composer-2.5", ServiceTier.STANDARD);
|
|
127
|
+
standard.addTurn(turn);
|
|
128
|
+
|
|
129
|
+
expect(unspecified.snapshot().estimatedCostUsd)
|
|
130
|
+
.toBe(standard.snapshot().estimatedCostUsd);
|
|
131
|
+
});
|
|
46
132
|
});
|