@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
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
} from "../../shared/
|
|
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,
|
|
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,
|
|
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:
|
|
651
|
+
credential: attachmentCredential,
|
|
624
652
|
backendEndpoint: config.stigmerBackendEndpoint,
|
|
625
653
|
});
|
|
626
654
|
if (attachment) {
|
|
627
|
-
const resolvedServers =
|
|
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(
|
|
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
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
+
}
|