@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.
Files changed (101) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent.js +85 -10
  3. package/dist/activities/call-agent.js.map +1 -1
  4. package/dist/activities/discover-mcp-server.js +9 -1
  5. package/dist/activities/discover-mcp-server.js.map +1 -1
  6. package/dist/activities/execute-cursor/index.d.ts +5 -0
  7. package/dist/activities/execute-cursor/index.js +88 -14
  8. package/dist/activities/execute-cursor/index.js.map +1 -1
  9. package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
  10. package/dist/activities/execute-cursor/model-pricing.js +19 -0
  11. package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
  12. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -0
  13. package/dist/activities/execute-cursor/prompt-builder.js +9 -0
  14. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  15. package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
  16. package/dist/activities/execute-cursor/service-tier.js +187 -0
  17. package/dist/activities/execute-cursor/service-tier.js.map +1 -0
  18. package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
  19. package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
  20. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  21. package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
  22. package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
  23. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +6 -0
  25. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  26. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/setup.js +45 -9
  28. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  29. package/dist/client/stigmer-client.d.ts +32 -1
  30. package/dist/client/stigmer-client.js +42 -2
  31. package/dist/client/stigmer-client.js.map +1 -1
  32. package/dist/main.js +18 -0
  33. package/dist/main.js.map +1 -1
  34. package/dist/runner.js +48 -0
  35. package/dist/runner.js.map +1 -1
  36. package/dist/sandbox-token-renewal.d.ts +65 -0
  37. package/dist/sandbox-token-renewal.js +169 -0
  38. package/dist/sandbox-token-renewal.js.map +1 -0
  39. package/dist/shared/artifact-storage.d.ts +17 -3
  40. package/dist/shared/artifact-storage.js +22 -4
  41. package/dist/shared/artifact-storage.js.map +1 -1
  42. package/dist/shared/caller-identity.d.ts +89 -0
  43. package/dist/shared/caller-identity.js +124 -0
  44. package/dist/shared/caller-identity.js.map +1 -0
  45. package/dist/shared/channel-attachment.d.ts +85 -0
  46. package/dist/shared/channel-attachment.js +203 -0
  47. package/dist/shared/channel-attachment.js.map +1 -0
  48. package/dist/shared/datastore-attachment.d.ts +2 -25
  49. package/dist/shared/datastore-attachment.js +1 -28
  50. package/dist/shared/datastore-attachment.js.map +1 -1
  51. package/dist/shared/synthesized-attachment.d.ts +51 -0
  52. package/dist/shared/synthesized-attachment.js +45 -0
  53. package/dist/shared/synthesized-attachment.js.map +1 -0
  54. package/dist/workflow-engine/loader.js +99 -2
  55. package/dist/workflow-engine/loader.js.map +1 -1
  56. package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
  57. package/dist/workflow-engine/tasks/call-agent.js +0 -2
  58. package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
  59. package/dist/workflow-engine/types.d.ts +39 -7
  60. package/dist/workflow-engine/types.js.map +1 -1
  61. package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
  62. package/dist/workflows/call-agent-orchestrator.js +8 -2
  63. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__test-utils__/mock-client.ts +4 -0
  66. package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
  67. package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
  68. package/src/activities/__tests__/call-agent.test.ts +219 -4
  69. package/src/activities/__tests__/discover-mcp-server.test.ts +49 -0
  70. package/src/activities/call-agent.ts +94 -10
  71. package/src/activities/discover-mcp-server.ts +13 -1
  72. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
  73. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
  74. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
  75. package/src/activities/execute-cursor/index.ts +121 -20
  76. package/src/activities/execute-cursor/model-pricing.ts +23 -0
  77. package/src/activities/execute-cursor/prompt-builder.ts +19 -0
  78. package/src/activities/execute-cursor/service-tier.ts +244 -0
  79. package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
  80. package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
  81. package/src/activities/execute-deep-agent/prompt-builder.ts +10 -0
  82. package/src/activities/execute-deep-agent/setup.ts +66 -10
  83. package/src/client/stigmer-client.ts +57 -4
  84. package/src/main.ts +20 -0
  85. package/src/runner.ts +62 -0
  86. package/src/sandbox-token-renewal.ts +212 -0
  87. package/src/shared/__tests__/caller-identity.test.ts +159 -0
  88. package/src/shared/__tests__/channel-attachment.test.ts +276 -0
  89. package/src/shared/__tests__/datastore-attachment.test.ts +4 -4
  90. package/src/shared/artifact-storage.ts +32 -7
  91. package/src/shared/caller-identity.ts +161 -0
  92. package/src/shared/channel-attachment.ts +237 -0
  93. package/src/shared/datastore-attachment.ts +2 -54
  94. package/src/shared/synthesized-attachment.ts +77 -0
  95. package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
  96. package/src/workflow-engine/__tests__/loader.test.ts +192 -7
  97. package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
  98. package/src/workflow-engine/loader.ts +113 -2
  99. package/src/workflow-engine/tasks/call-agent.ts +0 -2
  100. package/src/workflow-engine/types.ts +40 -7
  101. package/src/workflows/call-agent-orchestrator.ts +8 -2
@@ -49,6 +49,10 @@ import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "..
49
49
  import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
50
50
  import { readContextBridge } from "../../shared/context-bridge.js";
51
51
  import { readSenderIdentity } from "../../shared/sender-identity.js";
52
+ import {
53
+ injectCallerIdentityEnv,
54
+ resolveCallerIdentity,
55
+ } from "../../shared/caller-identity.js";
52
56
  import { readSessionContext } from "../../shared/session-context.js";
53
57
  import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
54
58
  import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
@@ -60,10 +64,12 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
60
64
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
61
65
  import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
62
66
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
67
+ import { synthesizeDatastoreAttachment } from "../../shared/datastore-attachment.js";
63
68
  import {
64
- injectDatastoreAttachment,
65
- synthesizeDatastoreAttachment,
66
- } from "../../shared/datastore-attachment.js";
69
+ discoverChannelMessaging,
70
+ synthesizeChannelAttachment,
71
+ } from "../../shared/channel-attachment.js";
72
+ import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
67
73
  import { mergeApprovalPolicies } from "./approval-policy.js";
68
74
  import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
69
75
  import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
@@ -115,6 +121,7 @@ import { statusProtoWriter } from "../../shared/execution-status-writer.js";
115
121
  import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
116
122
  import { closeProxySessions } from "./http2-interceptor.js";
117
123
  import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pricing.js";
124
+ import { resolveEffectiveServiceTier, resolveServiceTierParams } from "./service-tier.js";
118
125
  import { UsageAccumulator } from "./usage-accumulator.js";
119
126
  import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
120
127
  import { activityStarted, activityFinished } from "../../idle-watchdog.js";
@@ -590,11 +597,23 @@ async function executeCursorInner(
590
597
  }
591
598
  }
592
599
 
593
- // Phase 4: Resolve MCP servers with approval policies
600
+ // Phase 4: Resolve MCP servers with approval policies.
601
+ // The MCP-bound env map (and ONLY it — never the agent process env)
602
+ // carries the reserved caller-identity keys, so a server that declares
603
+ // them in spec.env can template the platform-verified caller into its
604
+ // headers. filterEnvToDeclaredKeys keeps every other server blind.
594
605
  await reportSetupProgress(client, executionId, "Resolving MCP servers");
595
606
  const transportPosture = resolveMcpTransportPosture(config.mode);
607
+ const mcpEnvVars = injectCallerIdentityEnv(
608
+ envVars,
609
+ resolveCallerIdentity(
610
+ blueprint.sessionSpec.metadata,
611
+ session.status?.audit?.specAudit?.createdBy,
612
+ ),
613
+ sessionId,
614
+ );
596
615
  let mcpResolution = await resolveMcpServers(
597
- client, blueprint.mergedMcpServerUsages, envVars, transportPosture,
616
+ client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
598
617
  );
599
618
  setupTiming.mark("resolve_mcp_servers");
600
619
 
@@ -602,11 +621,24 @@ async function executeCursorInner(
602
621
  heartbeatPhase = "resolving_mcp_servers";
603
622
  const sessionOrg = session.metadata?.org ?? "";
604
623
  mcpResolution = await backfillMcpServersIfNeeded(
605
- client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
624
+ client, mcpResolution, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
606
625
  transportPosture, heartbeat, secretKeys,
607
626
  );
608
627
  setupTiming.mark("backfill_mcp");
609
628
 
629
+ // The synthesized attachments' credential story (DD-006 D4): the
630
+ // exchanged token authenticates the discovery reads per-call (a
631
+ // desktop runner's ambient embedded_runner credential is refused by
632
+ // the messaging reach; undefined lets a cloud sandbox runner's
633
+ // ambient session-scoped token or OSS's no-auth apply). The
634
+ // attachment header falls back to the ambient credential where no
635
+ // exchange happens.
636
+ const exchangedRunnerToken =
637
+ await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
638
+ const attachmentCredential = exchangedRunnerToken
639
+ ?? config.stigmerTokenRef?.current
640
+ ?? config.stigmerToken;
641
+
610
642
  // Phase 4a2: Synthesize the datastore records attachment (T05).
611
643
  // Deliberately AFTER resolve + backfill: the attachment has no
612
644
  // McpServerUsage and reports discovered capabilities, so the
@@ -614,18 +646,38 @@ async function executeCursorInner(
614
646
  // delete_record (which on channels would be silently skipped).
615
647
  // Empty approval maps keep it approval-free by construction.
616
648
  if (blueprint.datastoreUsages.length > 0) {
617
- const scopedCredential =
618
- (await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
619
- ?? config.stigmerTokenRef?.current
620
- ?? config.stigmerToken;
621
649
  const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
622
650
  bridgeEndpoint: config.mcpBridgeEndpoint,
623
- credential: scopedCredential,
651
+ credential: attachmentCredential,
624
652
  backendEndpoint: config.stigmerBackendEndpoint,
625
653
  });
626
654
  if (attachment) {
627
- const resolvedServers = injectDatastoreAttachment(
628
- mcpResolution.resolvedServers, attachment,
655
+ const resolvedServers = injectSynthesizedAttachment(
656
+ mcpResolution.resolvedServers, attachment, "datastore records",
657
+ );
658
+ mcpResolution = {
659
+ resolvedServers,
660
+ cursorConfig: toCursorMcpConfig(resolvedServers),
661
+ };
662
+ }
663
+ }
664
+
665
+ // Phase 4a3: Synthesize the channel messaging attachment (DD-006
666
+ // D7/D8), the records attachment's twin. The discovery read is the
667
+ // attachment decision — the control plane runs the SAME candidate
668
+ // computation the send authorization uses — and every failure mode
669
+ // (no channel, OSS, registry down, pre-3a control plane) degrades
670
+ // to honest absence: no tool, no section, execution unharmed.
671
+ const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
672
+ if (channelMessaging.length > 0) {
673
+ const attachment = synthesizeChannelAttachment(channelMessaging, {
674
+ bridgeEndpoint: config.mcpBridgeEndpoint,
675
+ credential: attachmentCredential,
676
+ backendEndpoint: config.stigmerBackendEndpoint,
677
+ });
678
+ if (attachment) {
679
+ const resolvedServers = injectSynthesizedAttachment(
680
+ mcpResolution.resolvedServers, attachment, "channel messaging",
629
681
  );
630
682
  mcpResolution = {
631
683
  resolvedServers,
@@ -838,7 +890,9 @@ async function executeCursorInner(
838
890
  await ensurePricingLoaded();
839
891
  setupTiming.mark("load_pricing");
840
892
 
841
- // Phase 6: Validate model selection
893
+ // Phase 6: Validate model selection and resolve the service tier.
894
+ // UNSPECIFIED → STANDARD resolves here and nowhere else (#357): every
895
+ // upstream layer preserves the caller's raw enum value.
842
896
  const requestedModel = spec.executionConfig?.modelName || "default";
843
897
  const validatedModel = resolveModelId(requestedModel);
844
898
  if (validatedModel !== requestedModel) {
@@ -846,6 +900,7 @@ async function executeCursorInner(
846
900
  `ExecuteCursor model resolved: execution=${executionId}, requested="${requestedModel}", using="${validatedModel}"`,
847
901
  );
848
902
  }
903
+ const requestedServiceTier = resolveEffectiveServiceTier(spec.executionConfig?.serviceTier);
849
904
 
850
905
  heartbeat();
851
906
 
@@ -880,10 +935,21 @@ async function executeCursorInner(
880
935
  );
881
936
  }
882
937
 
938
+ // Translate the tier into the explicit variant params sent with every
939
+ // create/resume. Never a bare { id }: the catalog's default variant is
940
+ // account-influenced and picks the price (#357).
941
+ const modelParams = await resolveServiceTierParams({
942
+ apiKey: effectiveApiKey,
943
+ modelId: validatedModel,
944
+ tier: requestedServiceTier,
945
+ executionId,
946
+ });
947
+
883
948
  const createOptions: CreateAgentOptions | CreateCloudAgentOptions = agentMode === "cloud"
884
949
  ? {
885
950
  apiKey: effectiveApiKey,
886
951
  model: validatedModel || undefined,
952
+ modelParams,
887
953
  repos: blueprint.cloudRepos,
888
954
  sessionId,
889
955
  mcpServers: mcpConfig,
@@ -892,6 +958,7 @@ async function executeCursorInner(
892
958
  : {
893
959
  apiKey: effectiveApiKey,
894
960
  model: validatedModel,
961
+ modelParams,
895
962
  workspaceDirs: blueprint.workspaceDirs,
896
963
  sessionId,
897
964
  workspaceRootDir: config.workspaceRootDir,
@@ -992,6 +1059,7 @@ async function executeCursorInner(
992
1059
  userMessage: spec.message,
993
1060
  skills: skillMetadata,
994
1061
  datastoreUsages: blueprint.datastoreUsages,
1062
+ channelMessaging,
995
1063
  subAgents: blueprint.subAgents,
996
1064
  workspaceDirs: blueprint.workspaceDirs,
997
1065
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
@@ -1023,7 +1091,11 @@ async function executeCursorInner(
1023
1091
 
1024
1092
  // Phase 10b: Initialize usage accumulator for runner-side token tracking
1025
1093
  await ensurePricingLoaded();
1026
- const usageAccumulator = new UsageAccumulator(validatedModel);
1094
+ const usageAccumulator = new UsageAccumulator(
1095
+ validatedModel,
1096
+ requestedServiceTier,
1097
+ modelParams,
1098
+ );
1027
1099
 
1028
1100
  // Phase 10c: Start OTel turn span. Coarse-grained — spans the whole turn
1029
1101
  // (agent.send + stream + any recovery retry + the turn boundary), ended once
@@ -1525,14 +1597,36 @@ async function executeCursorInner(
1525
1597
 
1526
1598
  // Phase 13: Map final result
1527
1599
  const result = await run.wait();
1528
- const sdkResolvedModel = result.model?.id || undefined;
1529
1600
  console.log(
1530
1601
  `ExecuteCursor run.wait() result: execution=${executionId}, result=${JSON.stringify(result)}`,
1531
1602
  );
1532
- if (sdkResolvedModel && sdkResolvedModel !== validatedModel) {
1533
- console.log(
1534
- `ExecuteCursor model divergence: execution=${executionId}, requested=${validatedModel}, sdkResolved=${sdkResolvedModel}`,
1535
- );
1603
+ // Echo sanity check only: result.model ECHOES the requested selection —
1604
+ // the SDK never reports the variant that actually served the call
1605
+ // (verified against the billing ledger, #357). A mismatch here means the
1606
+ // SDK rewrote our selection (contract change), not variant drift; the
1607
+ // authoritative requested-vs-billed reconciliation is the cloud billing
1608
+ // handler's pricing_variant mismatch metric.
1609
+ const echoedSelection = result.model;
1610
+ if (echoedSelection) {
1611
+ const idMatches = echoedSelection.id === validatedModel;
1612
+ // Compare id/value pairs explicitly, never serialized objects: the SDK
1613
+ // may add fields to ModelParameterValue or reorder keys, and neither
1614
+ // is contract drift.
1615
+ const echoedParams = [...(echoedSelection.params ?? [])]
1616
+ .sort((a, b) => a.id.localeCompare(b.id));
1617
+ const paramsMatch =
1618
+ echoedParams.length === modelParams.length &&
1619
+ echoedParams.every(
1620
+ (p, i) => p.id === modelParams[i].id && p.value === modelParams[i].value,
1621
+ );
1622
+ if (!idMatches || !paramsMatch) {
1623
+ console.warn(
1624
+ `ExecuteCursor model selection echo mismatch (SDK contract drift?): ` +
1625
+ `execution=${executionId}, ` +
1626
+ `requested=${JSON.stringify({ id: validatedModel, params: modelParams })}, ` +
1627
+ `echoed=${JSON.stringify(echoedSelection)}`,
1628
+ );
1629
+ }
1536
1630
  }
1537
1631
  status.completedAt = utcTimestamp();
1538
1632
 
@@ -1605,6 +1699,7 @@ async function executeCursorInner(
1605
1699
  userMessage: spec.message,
1606
1700
  skills: skillMetadata,
1607
1701
  datastoreUsages: blueprint.datastoreUsages,
1702
+ channelMessaging,
1608
1703
  subAgents: blueprint.subAgents,
1609
1704
  workspaceDirs: blueprint.workspaceDirs,
1610
1705
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
@@ -2169,6 +2264,11 @@ export interface BuildPromptInput {
2169
2264
  skills: import("./prompt-builder.js").SkillMetadata[];
2170
2265
  /** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
2171
2266
  datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
2267
+ /**
2268
+ * Serving proactive channels + their templates (the DD-006 D2
2269
+ * discovery read) — the `<available_channel_templates>` section.
2270
+ */
2271
+ channelMessaging?: import("../../shared/channel-attachment.js").ChannelMessagingInfo[];
2172
2272
  subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
2173
2273
  workspaceDirs: string[];
2174
2274
  workspaceFileRefs: string[];
@@ -2276,6 +2376,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2276
2376
  userMessage,
2277
2377
  skills,
2278
2378
  datastoreUsages: input.datastoreUsages ?? [],
2379
+ channelMessaging: input.channelMessaging ?? [],
2279
2380
  subAgents,
2280
2381
  workspaceDirs,
2281
2382
  workspaceFileRefs,
@@ -139,6 +139,29 @@ export function getCursorModelPricing(model: string): CursorModelPricing {
139
139
  return { ...DEFAULT_PRICING, model };
140
140
  }
141
141
 
142
+ /**
143
+ * Look up pricing for a Cursor model under an explicitly requested speed
144
+ * variant (stigmer/stigmer#357). Unlike the suffix inference above — which
145
+ * derives the variant from a wire id like "composer-2.5-fast" — this is for
146
+ * callers that KNOW the variant because they requested it. Falls back to
147
+ * base rates (with a warning) when the registry prices no such variant;
148
+ * create-time validation makes that unreachable short of registry drift.
149
+ */
150
+ export function getCursorModelPricingForVariant(
151
+ model: string,
152
+ variant: "fast" | null,
153
+ ): CursorModelPricing {
154
+ const base = getCursorModelPricing(model);
155
+ if (variant !== "fast") return base;
156
+ const fast = applyFastVariant(base, model);
157
+ if (fast) return fast;
158
+ console.warn(
159
+ `Requested fast-variant pricing for "${model}" but the registry prices no fast variant — ` +
160
+ `estimating at base rates (billing reconciliation remains authoritative)`,
161
+ );
162
+ return base;
163
+ }
164
+
142
165
  /**
143
166
  * Compute USD cost for a single turn.
144
167
  *
@@ -22,6 +22,10 @@ import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentex
22
22
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
23
23
  import { formatContextBridgeText } from "../../shared/context-bridge.js";
24
24
  import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
25
+ import {
26
+ formatChannelTemplatesSection,
27
+ type ChannelMessagingInfo,
28
+ } from "../../shared/channel-attachment.js";
25
29
  import {
26
30
  formatSenderIdentityText,
27
31
  type SenderIdentity,
@@ -61,6 +65,12 @@ export interface EnhancedPromptOptions {
61
65
  * pointing the model at the synthesized record tools.
62
66
  */
63
67
  datastoreUsages?: DatastoreUsage[];
68
+ /**
69
+ * Serving proactive channels + their approved templates — rendered as
70
+ * the `<available_channel_templates>` section (proactive-messaging
71
+ * DD-003 D5) beside the synthesized send_channel_message tool.
72
+ */
73
+ channelMessaging?: ChannelMessagingInfo[];
64
74
  subAgents: SubAgent[];
65
75
  workspaceDirs: string[];
66
76
  workspaceFileRefs: string[];
@@ -133,6 +143,15 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
133
143
  sections.push(formatDatastoresSection(options.datastoreUsages));
134
144
  }
135
145
 
146
+ if (options.channelMessaging !== undefined && options.channelMessaging.length > 0) {
147
+ // "" when nothing is sendable — the tool alone still serves text
148
+ // sends inside a 24-hour window (DD-006 D6).
149
+ const channelSection = formatChannelTemplatesSection(options.channelMessaging);
150
+ if (channelSection !== "") {
151
+ sections.push(channelSection);
152
+ }
153
+ }
154
+
136
155
  if (options.subAgents.length > 0) {
137
156
  sections.push(formatSubAgentsSection(options.subAgents));
138
157
  }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Service-tier → Cursor variant-parameter translation (stigmer/stigmer#357).
3
+ *
4
+ * The platform contract: an execution's model selection is ALWAYS explicit.
5
+ * A bare `{ id }` lets the Cursor catalog's default variant decide the price
6
+ * (observed 2026-08-06: composer-2.5 defaults to fast=true at ~4x base
7
+ * rates, claude-haiku-4-5 to thinking=true), and that default follows an
8
+ * out-of-band account setting. This module pins every price-bearing variant
9
+ * parameter the model declares, so the billed variant is a deterministic
10
+ * function of ExecutionConfig.service_tier:
11
+ *
12
+ * - STANDARD: every price-bearing boolean pinned to its base value
13
+ * (fast=false, thinking=false where the parameter exists).
14
+ * - FAST: fast=true, thinking still pinned false.
15
+ * - Price-neutral parameters (e.g. effort) are deliberately NOT pinned —
16
+ * they follow the catalog default and do not change the bill.
17
+ *
18
+ * Parameter bundles come from Cursor.models.list() (worker-cached): the
19
+ * catalog is the only source of a model's parameter ids, and the fetch
20
+ * rides the same proxy fetch-interceptor as every other SDK call, so it
21
+ * works identically in proxy and direct modes.
22
+ *
23
+ * UNSPECIFIED resolves to STANDARD here and ONLY here — every upstream
24
+ * layer preserves the caller's raw enum so "user chose standard" stays
25
+ * distinguishable from "platform default" all the way to the ledger.
26
+ */
27
+
28
+ import { Cursor } from "@cursor/sdk";
29
+ import type { ModelListItem, ModelParameterValue } from "@cursor/sdk";
30
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
31
+
32
+ /**
33
+ * The effective tier after platform-default resolution: never UNSPECIFIED.
34
+ */
35
+ export type EffectiveServiceTier = ServiceTier.STANDARD | ServiceTier.FAST;
36
+
37
+ /**
38
+ * Catalog ids that mean "Cursor picks the model" (Auto). Auto's single
39
+ * catalog variant has an empty parameter set — there is no tier dimension
40
+ * to pin, a documented v1 limitation. Create-time validation refuses FAST
41
+ * without a pinned model, so FAST can only reach Auto through registry
42
+ * drift — handled as a loud failure below.
43
+ */
44
+ const AUTO_MODEL_IDS = new Set(["default", "auto"]);
45
+
46
+ /**
47
+ * Variant parameter ids that change the per-token price. Pinning exactly
48
+ * these keeps the bill deterministic while leaving latency/effort knobs on
49
+ * their catalog defaults. Sourced from the Cursor catalog survey
50
+ * (stigmer-cloud _projects/2026-08/20260806.04.model-service-tier).
51
+ */
52
+ const FAST_PARAM_ID = "fast";
53
+ const THINKING_PARAM_ID = "thinking";
54
+
55
+ const CATALOG_CACHE_TTL_MS = 3_600_000; // 1 hour, matching the pricing table TTL
56
+
57
+ interface CatalogCache {
58
+ readonly apiKey: string;
59
+ readonly models: readonly ModelListItem[];
60
+ readonly expiresAt: number;
61
+ }
62
+
63
+ let catalogCache: CatalogCache | null = null;
64
+ // Keyed by apiKey: a worker normally serves one org's key, but nothing
65
+ // enforces that — handing execution A a catalog fetched under execution B's
66
+ // key would leak another account's model availability into A's translation.
67
+ let inflightCatalogFetch: { readonly apiKey: string; readonly promise: Promise<readonly ModelListItem[]> } | null = null;
68
+
69
+ /** Test-only: drop the worker-level catalog cache. */
70
+ export function resetCatalogCacheForTests(): void {
71
+ catalogCache = null;
72
+ inflightCatalogFetch = null;
73
+ }
74
+
75
+ /**
76
+ * Resolve the configured tier to its effective value. The single place in
77
+ * the platform where UNSPECIFIED becomes STANDARD.
78
+ */
79
+ export function resolveEffectiveServiceTier(
80
+ configured: ServiceTier | undefined,
81
+ ): EffectiveServiceTier {
82
+ return configured === ServiceTier.FAST ? ServiceTier.FAST : ServiceTier.STANDARD;
83
+ }
84
+
85
+ /** Human-readable tier label for logs and error messages. */
86
+ export function serviceTierLabel(tier: ServiceTier): string {
87
+ switch (tier) {
88
+ case ServiceTier.FAST:
89
+ return "fast";
90
+ case ServiceTier.STANDARD:
91
+ return "standard";
92
+ default:
93
+ return "unspecified";
94
+ }
95
+ }
96
+
97
+ async function listCatalogModels(apiKey: string): Promise<readonly ModelListItem[]> {
98
+ const now = Date.now();
99
+ if (catalogCache && catalogCache.apiKey === apiKey && catalogCache.expiresAt > now) {
100
+ return catalogCache.models;
101
+ }
102
+ if (inflightCatalogFetch?.apiKey === apiKey) return inflightCatalogFetch.promise;
103
+
104
+ let entry: { apiKey: string; promise: Promise<readonly ModelListItem[]> } | null = null;
105
+ const promise = (async () => {
106
+ try {
107
+ const models = await Cursor.models.list({ apiKey });
108
+ catalogCache = { apiKey, models, expiresAt: Date.now() + CATALOG_CACHE_TTL_MS };
109
+ return models;
110
+ } finally {
111
+ // Clear only our own entry — a concurrent fetch under another key may
112
+ // have replaced it.
113
+ if (inflightCatalogFetch === entry) {
114
+ inflightCatalogFetch = null;
115
+ }
116
+ }
117
+ })();
118
+ entry = { apiKey, promise };
119
+ inflightCatalogFetch = entry;
120
+ return promise;
121
+ }
122
+
123
+ function findCatalogModel(
124
+ models: readonly ModelListItem[],
125
+ modelId: string,
126
+ ): ModelListItem | undefined {
127
+ return models.find((m) => m.id === modelId || m.aliases?.includes(modelId));
128
+ }
129
+
130
+ export interface ResolveServiceTierParamsOptions {
131
+ readonly apiKey: string;
132
+ /** Validated model id the execution runs on (may be "default" for Auto). */
133
+ readonly modelId: string;
134
+ readonly tier: EffectiveServiceTier;
135
+ /** For log correlation only. */
136
+ readonly executionId: string;
137
+ }
138
+
139
+ /**
140
+ * Translate the effective tier into the explicit variant parameters to send
141
+ * with every Agent.create/resume for this execution.
142
+ *
143
+ * Fail-closed posture: FAST with no pinnable fast dimension is an error,
144
+ * never a silent downgrade — create-time validation makes this unreachable
145
+ * unless the registry and the provider catalog have drifted, and that drift
146
+ * must be heard about, not absorbed.
147
+ *
148
+ * STANDARD degrades to empty params on catalog failures rather than failing
149
+ * the execution — but be clear about what that costs: an unpinned selection
150
+ * falls to the catalog default variant, which for several models IS the
151
+ * fast/thinking variant at multiples of base rates (the incident this module
152
+ * exists to prevent). Failing every standard execution whenever the catalog
153
+ * endpoint blips would be the worse trade; the WARN below plus billing's
154
+ * requested-vs-billed mismatch alarm (which catches exactly this window)
155
+ * are the compensating controls.
156
+ */
157
+ export async function resolveServiceTierParams(
158
+ options: ResolveServiceTierParamsOptions,
159
+ ): Promise<ModelParameterValue[]> {
160
+ const { apiKey, modelId, tier, executionId } = options;
161
+ const tierName = serviceTierLabel(tier);
162
+
163
+ if (AUTO_MODEL_IDS.has(modelId)) {
164
+ if (tier === ServiceTier.FAST) {
165
+ throw new Error(
166
+ `service_tier=fast requires a pinned model — Auto ("${modelId}") has no ` +
167
+ `tier dimension. Execution ${executionId} should have been refused at ` +
168
+ `create time; the model registry and provider catalog may have drifted.`,
169
+ );
170
+ }
171
+ console.log(
172
+ `ServiceTier: execution=${executionId} model=${modelId} tier=${tierName} — ` +
173
+ `Auto has no variant parameters; Cursor picks the model and variant ` +
174
+ `(documented v1 limitation).`,
175
+ );
176
+ return [];
177
+ }
178
+
179
+ let models: readonly ModelListItem[];
180
+ try {
181
+ models = await listCatalogModels(apiKey);
182
+ } catch (err) {
183
+ if (tier === ServiceTier.FAST) {
184
+ throw new Error(
185
+ `service_tier=fast for execution ${executionId} needs the Cursor model ` +
186
+ `catalog to resolve variant params for "${modelId}", and the catalog ` +
187
+ `fetch failed: ${err instanceof Error ? err.message : String(err)}`,
188
+ );
189
+ }
190
+ console.warn(
191
+ `ServiceTier UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} — ` +
192
+ `catalog fetch failed (${err instanceof Error ? err.message : err}); ` +
193
+ `sending no variant params, so the catalog DEFAULT variant decides the ` +
194
+ `price for this execution (fast/thinking on several models — the ` +
195
+ `expensive direction). Billing's requested-vs-billed mismatch alarm ` +
196
+ `covers this window.`,
197
+ );
198
+ return [];
199
+ }
200
+
201
+ const model = findCatalogModel(models, modelId);
202
+ if (!model) {
203
+ if (tier === ServiceTier.FAST) {
204
+ throw new Error(
205
+ `service_tier=fast requested for "${modelId}" (execution ${executionId}) ` +
206
+ `but the Cursor catalog does not list that model — cannot pin a fast ` +
207
+ `variant. The model registry and provider catalog have drifted.`,
208
+ );
209
+ }
210
+ console.warn(
211
+ `ServiceTier UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} — ` +
212
+ `model not in the Cursor catalog; sending no variant params, so the ` +
213
+ `catalog DEFAULT variant decides the price for this execution.`,
214
+ );
215
+ return [];
216
+ }
217
+
218
+ const params: ModelParameterValue[] = [];
219
+ for (const def of model.parameters ?? []) {
220
+ if (def.id === FAST_PARAM_ID) {
221
+ params.push({ id: FAST_PARAM_ID, value: tier === ServiceTier.FAST ? "true" : "false" });
222
+ } else if (def.id === THINKING_PARAM_ID) {
223
+ params.push({ id: THINKING_PARAM_ID, value: "false" });
224
+ }
225
+ // Any other parameter (e.g. effort) is price-neutral: left to the
226
+ // catalog default variant on purpose.
227
+ }
228
+
229
+ if (tier === ServiceTier.FAST && !params.some((p) => p.id === FAST_PARAM_ID)) {
230
+ throw new Error(
231
+ `service_tier=fast requested for "${modelId}" (execution ${executionId}) ` +
232
+ `but the Cursor catalog declares no "fast" parameter for it. The model ` +
233
+ `registry prices a fast variant the provider no longer offers — refusing ` +
234
+ `rather than silently billing an unknown variant.`,
235
+ );
236
+ }
237
+
238
+ params.sort((a, b) => a.id.localeCompare(b.id));
239
+ console.log(
240
+ `ServiceTier: execution=${executionId} model=${modelId} tier=${tierName} ` +
241
+ `params=${JSON.stringify(params)}`,
242
+ );
243
+ return params;
244
+ }