@stigmer/runner 3.6.0 → 3.8.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/execute-cursor/index.d.ts +12 -0
- package/dist/activities/execute-cursor/index.js +80 -10
- 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 +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js +11 -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/mcp-gate.d.ts +28 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js +22 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -0
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +11 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +30 -4
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +6 -1
- package/dist/client/stigmer-client.js +5 -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/channel-attachment.d.ts +3 -1
- package/dist/shared/channel-attachment.js +3 -1
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/conversation-attachment.d.ts +81 -0
- package/dist/shared/conversation-attachment.js +102 -0
- package/dist/shared/conversation-attachment.js.map +1 -0
- package/dist/shared/conversation-catchup.d.ts +33 -0
- package/dist/shared/conversation-catchup.js +53 -0
- package/dist/shared/conversation-catchup.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/__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/call-agent.ts +94 -10
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
- 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 +111 -11
- package/src/activities/execute-cursor/model-pricing.ts +23 -0
- package/src/activities/execute-cursor/prompt-builder.ts +23 -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/__tests__/mcp-gate.test.ts +42 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +39 -1
- package/src/activities/execute-deep-agent/mcp-gate.ts +37 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -2
- package/src/activities/execute-deep-agent/setup.ts +40 -4
- package/src/client/stigmer-client.ts +11 -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__/channel-attachment.test.ts +3 -3
- package/src/shared/__tests__/conversation-attachment.test.ts +138 -0
- package/src/shared/__tests__/conversation-catchup.test.ts +70 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +120 -0
- package/src/shared/artifact-storage.ts +32 -7
- package/src/shared/channel-attachment.ts +3 -1
- package/src/shared/conversation-attachment.ts +115 -0
- package/src/shared/conversation-catchup.ts +60 -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
|
@@ -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
|
});
|
|
@@ -48,6 +48,7 @@ import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantTo
|
|
|
48
48
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
49
49
|
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
50
50
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
51
|
+
import { readConversationCatchup } from "../../shared/conversation-catchup.js";
|
|
51
52
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
52
53
|
import {
|
|
53
54
|
injectCallerIdentityEnv,
|
|
@@ -69,6 +70,10 @@ import {
|
|
|
69
70
|
discoverChannelMessaging,
|
|
70
71
|
synthesizeChannelAttachment,
|
|
71
72
|
} from "../../shared/channel-attachment.js";
|
|
73
|
+
import {
|
|
74
|
+
readChannelConversationId,
|
|
75
|
+
synthesizeConversationAttachment,
|
|
76
|
+
} from "../../shared/conversation-attachment.js";
|
|
72
77
|
import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
|
|
73
78
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
74
79
|
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
@@ -79,7 +84,7 @@ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
|
79
84
|
import { resolveSkills } from "./skill-resolver.js";
|
|
80
85
|
import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
|
|
81
86
|
import { resolveAttachments } from "./attachment-resolver.js";
|
|
82
|
-
import { buildEnhancedPrompt, buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
87
|
+
import { buildEnhancedPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
83
88
|
import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
|
|
84
89
|
import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
|
|
85
90
|
import {
|
|
@@ -121,6 +126,7 @@ import { statusProtoWriter } from "../../shared/execution-status-writer.js";
|
|
|
121
126
|
import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
|
|
122
127
|
import { closeProxySessions } from "./http2-interceptor.js";
|
|
123
128
|
import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pricing.js";
|
|
129
|
+
import { resolveEffectiveServiceTier, resolveServiceTierParams } from "./service-tier.js";
|
|
124
130
|
import { UsageAccumulator } from "./usage-accumulator.js";
|
|
125
131
|
import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
126
132
|
import { activityStarted, activityFinished } from "../../idle-watchdog.js";
|
|
@@ -684,6 +690,31 @@ async function executeCursorInner(
|
|
|
684
690
|
};
|
|
685
691
|
}
|
|
686
692
|
}
|
|
693
|
+
|
|
694
|
+
// Phase 4a4: Synthesize the conversation participation attachment
|
|
695
|
+
// (channel-conversations DD-008 D-c) — the third sibling. The
|
|
696
|
+
// channel-id session label IS the attachment decision (stamped
|
|
697
|
+
// server-side on every channel session; a free local read, unlike
|
|
698
|
+
// the channels discovery RPC above). HTTP-only: synthesize answers
|
|
699
|
+
// undefined with no bridge endpoint by design (see
|
|
700
|
+
// shared/conversation-attachment.ts).
|
|
701
|
+
const conversationAttachment = synthesizeConversationAttachment(
|
|
702
|
+
readChannelConversationId(session.metadata?.labels),
|
|
703
|
+
{
|
|
704
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
705
|
+
credential: attachmentCredential,
|
|
706
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
if (conversationAttachment) {
|
|
710
|
+
const resolvedServers = injectSynthesizedAttachment(
|
|
711
|
+
mcpResolution.resolvedServers, conversationAttachment, "conversation participation",
|
|
712
|
+
);
|
|
713
|
+
mcpResolution = {
|
|
714
|
+
resolvedServers,
|
|
715
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
687
718
|
const mcpConfig = mcpResolution.cursorConfig;
|
|
688
719
|
|
|
689
720
|
// Phase 4b: Merge approval policies from all layers.
|
|
@@ -889,7 +920,9 @@ async function executeCursorInner(
|
|
|
889
920
|
await ensurePricingLoaded();
|
|
890
921
|
setupTiming.mark("load_pricing");
|
|
891
922
|
|
|
892
|
-
// Phase 6: Validate model selection
|
|
923
|
+
// Phase 6: Validate model selection and resolve the service tier.
|
|
924
|
+
// UNSPECIFIED → STANDARD resolves here and nowhere else (#357): every
|
|
925
|
+
// upstream layer preserves the caller's raw enum value.
|
|
893
926
|
const requestedModel = spec.executionConfig?.modelName || "default";
|
|
894
927
|
const validatedModel = resolveModelId(requestedModel);
|
|
895
928
|
if (validatedModel !== requestedModel) {
|
|
@@ -897,6 +930,7 @@ async function executeCursorInner(
|
|
|
897
930
|
`ExecuteCursor model resolved: execution=${executionId}, requested="${requestedModel}", using="${validatedModel}"`,
|
|
898
931
|
);
|
|
899
932
|
}
|
|
933
|
+
const requestedServiceTier = resolveEffectiveServiceTier(spec.executionConfig?.serviceTier);
|
|
900
934
|
|
|
901
935
|
heartbeat();
|
|
902
936
|
|
|
@@ -931,10 +965,21 @@ async function executeCursorInner(
|
|
|
931
965
|
);
|
|
932
966
|
}
|
|
933
967
|
|
|
968
|
+
// Translate the tier into the explicit variant params sent with every
|
|
969
|
+
// create/resume. Never a bare { id }: the catalog's default variant is
|
|
970
|
+
// account-influenced and picks the price (#357).
|
|
971
|
+
const modelParams = await resolveServiceTierParams({
|
|
972
|
+
apiKey: effectiveApiKey,
|
|
973
|
+
modelId: validatedModel,
|
|
974
|
+
tier: requestedServiceTier,
|
|
975
|
+
executionId,
|
|
976
|
+
});
|
|
977
|
+
|
|
934
978
|
const createOptions: CreateAgentOptions | CreateCloudAgentOptions = agentMode === "cloud"
|
|
935
979
|
? {
|
|
936
980
|
apiKey: effectiveApiKey,
|
|
937
981
|
model: validatedModel || undefined,
|
|
982
|
+
modelParams,
|
|
938
983
|
repos: blueprint.cloudRepos,
|
|
939
984
|
sessionId,
|
|
940
985
|
mcpServers: mcpConfig,
|
|
@@ -943,6 +988,7 @@ async function executeCursorInner(
|
|
|
943
988
|
: {
|
|
944
989
|
apiKey: effectiveApiKey,
|
|
945
990
|
model: validatedModel,
|
|
991
|
+
modelParams,
|
|
946
992
|
workspaceDirs: blueprint.workspaceDirs,
|
|
947
993
|
sessionId,
|
|
948
994
|
workspaceRootDir: config.workspaceRootDir,
|
|
@@ -1055,6 +1101,7 @@ async function executeCursorInner(
|
|
|
1055
1101
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1056
1102
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1057
1103
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1104
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1058
1105
|
});
|
|
1059
1106
|
|
|
1060
1107
|
// Phase 10a: Inject structured output instruction for Cursor harness
|
|
@@ -1075,7 +1122,11 @@ async function executeCursorInner(
|
|
|
1075
1122
|
|
|
1076
1123
|
// Phase 10b: Initialize usage accumulator for runner-side token tracking
|
|
1077
1124
|
await ensurePricingLoaded();
|
|
1078
|
-
const usageAccumulator = new UsageAccumulator(
|
|
1125
|
+
const usageAccumulator = new UsageAccumulator(
|
|
1126
|
+
validatedModel,
|
|
1127
|
+
requestedServiceTier,
|
|
1128
|
+
modelParams,
|
|
1129
|
+
);
|
|
1079
1130
|
|
|
1080
1131
|
// Phase 10c: Start OTel turn span. Coarse-grained — spans the whole turn
|
|
1081
1132
|
// (agent.send + stream + any recovery retry + the turn boundary), ended once
|
|
@@ -1577,14 +1628,36 @@ async function executeCursorInner(
|
|
|
1577
1628
|
|
|
1578
1629
|
// Phase 13: Map final result
|
|
1579
1630
|
const result = await run.wait();
|
|
1580
|
-
const sdkResolvedModel = result.model?.id || undefined;
|
|
1581
1631
|
console.log(
|
|
1582
1632
|
`ExecuteCursor run.wait() result: execution=${executionId}, result=${JSON.stringify(result)}`,
|
|
1583
1633
|
);
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1634
|
+
// Echo sanity check only: result.model ECHOES the requested selection —
|
|
1635
|
+
// the SDK never reports the variant that actually served the call
|
|
1636
|
+
// (verified against the billing ledger, #357). A mismatch here means the
|
|
1637
|
+
// SDK rewrote our selection (contract change), not variant drift; the
|
|
1638
|
+
// authoritative requested-vs-billed reconciliation is the cloud billing
|
|
1639
|
+
// handler's pricing_variant mismatch metric.
|
|
1640
|
+
const echoedSelection = result.model;
|
|
1641
|
+
if (echoedSelection) {
|
|
1642
|
+
const idMatches = echoedSelection.id === validatedModel;
|
|
1643
|
+
// Compare id/value pairs explicitly, never serialized objects: the SDK
|
|
1644
|
+
// may add fields to ModelParameterValue or reorder keys, and neither
|
|
1645
|
+
// is contract drift.
|
|
1646
|
+
const echoedParams = [...(echoedSelection.params ?? [])]
|
|
1647
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1648
|
+
const paramsMatch =
|
|
1649
|
+
echoedParams.length === modelParams.length &&
|
|
1650
|
+
echoedParams.every(
|
|
1651
|
+
(p, i) => p.id === modelParams[i].id && p.value === modelParams[i].value,
|
|
1652
|
+
);
|
|
1653
|
+
if (!idMatches || !paramsMatch) {
|
|
1654
|
+
console.warn(
|
|
1655
|
+
`ExecuteCursor model selection echo mismatch (SDK contract drift?): ` +
|
|
1656
|
+
`execution=${executionId}, ` +
|
|
1657
|
+
`requested=${JSON.stringify({ id: validatedModel, params: modelParams })}, ` +
|
|
1658
|
+
`echoed=${JSON.stringify(echoedSelection)}`,
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1588
1661
|
}
|
|
1589
1662
|
status.completedAt = utcTimestamp();
|
|
1590
1663
|
|
|
@@ -1664,9 +1737,15 @@ async function executeCursorInner(
|
|
|
1664
1737
|
attachmentPaths,
|
|
1665
1738
|
pendingApprovals: adjudicatedApprovals,
|
|
1666
1739
|
interactionMode,
|
|
1740
|
+
// buildFromPlan was silently dropped here until T03 Sitting 3 —
|
|
1741
|
+
// a build turn that hit handle recovery lost its directive. The
|
|
1742
|
+
// fresh prompt must carry every per-turn directive the original
|
|
1743
|
+
// did.
|
|
1744
|
+
buildFromPlan,
|
|
1667
1745
|
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
1668
1746
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1669
1747
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1748
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1670
1749
|
});
|
|
1671
1750
|
|
|
1672
1751
|
console.log(
|
|
@@ -2263,6 +2342,18 @@ export interface BuildPromptInput {
|
|
|
2263
2342
|
* turn.
|
|
2264
2343
|
*/
|
|
2265
2344
|
sessionContext?: string;
|
|
2345
|
+
/**
|
|
2346
|
+
* Conversation catchup from the execution spec's `conversation_catchup`
|
|
2347
|
+
* (cloud DD-006): what happened on the channel conversation that the
|
|
2348
|
+
* agent has not seen. PER-TURN, so unlike the three standing values
|
|
2349
|
+
* above it rides BOTH prompt paths — the enhanced prompt and a resumed
|
|
2350
|
+
* turn's prefix (the `interaction_mode` shape). Handback lands
|
|
2351
|
+
* mid-session on a resumed agent: the resumed path is the one that
|
|
2352
|
+
* matters. Once delivered, the digest persists in the agent's own
|
|
2353
|
+
* conversation store; the next turn's field is composed fresh and is
|
|
2354
|
+
* usually blank.
|
|
2355
|
+
*/
|
|
2356
|
+
conversationCatchup?: string;
|
|
2266
2357
|
}
|
|
2267
2358
|
|
|
2268
2359
|
/**
|
|
@@ -2294,6 +2385,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2294
2385
|
attachmentPaths,
|
|
2295
2386
|
interactionMode,
|
|
2296
2387
|
buildFromPlan,
|
|
2388
|
+
conversationCatchup,
|
|
2297
2389
|
} = input;
|
|
2298
2390
|
|
|
2299
2391
|
const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
@@ -2311,15 +2403,22 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2311
2403
|
|
|
2312
2404
|
// A successfully resumed agent carries its own conversation context via the
|
|
2313
2405
|
// SDK's native store — send the raw user message with no preamble. The
|
|
2314
|
-
// exceptions are the per-EXECUTION
|
|
2406
|
+
// exceptions are the per-EXECUTION values, which never inherit from the
|
|
2315
2407
|
// session's first turn: the interaction-mode prefix (a follow-up can switch
|
|
2316
2408
|
// Agent→Plan mid-session, and for Cursor the prompt is the only plan-mode
|
|
2317
|
-
// enforcement)
|
|
2318
|
-
//
|
|
2409
|
+
// enforcement), the implement-plan directive (the build turn is usually a
|
|
2410
|
+
// follow-up on a resumed agent), and the conversation catchup (handback
|
|
2411
|
+
// ALWAYS lands mid-session on a resumed agent — this prefix is the property
|
|
2412
|
+
// the metadata lane structurally cannot deliver, cloud DD-006). Catchup
|
|
2413
|
+
// last: it is context, and context sits closest to the task (the enhanced
|
|
2414
|
+
// prompt's own ordering doctrine).
|
|
2319
2415
|
if (resolution.reason === "resumed_successfully") {
|
|
2320
2416
|
const prefixes = [
|
|
2321
2417
|
formatInteractionModePrefix(interactionMode),
|
|
2322
2418
|
formatImplementPlanSection(buildFromPlan, attachmentPaths),
|
|
2419
|
+
conversationCatchup !== undefined
|
|
2420
|
+
? formatConversationCatchupSection(conversationCatchup)
|
|
2421
|
+
: undefined,
|
|
2323
2422
|
].filter((p): p is string => p !== undefined);
|
|
2324
2423
|
return prefixes.length > 0
|
|
2325
2424
|
? [...prefixes, userMessage].join("\n\n")
|
|
@@ -2344,6 +2443,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2344
2443
|
contextBridge: input.contextBridge,
|
|
2345
2444
|
senderIdentity: input.senderIdentity,
|
|
2346
2445
|
sessionContext: input.sessionContext,
|
|
2446
|
+
conversationCatchup,
|
|
2347
2447
|
});
|
|
2348
2448
|
}
|
|
2349
2449
|
|
|
@@ -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
|
*
|
|
@@ -21,6 +21,7 @@ import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agenti
|
|
|
21
21
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
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
|
+
import { formatConversationCatchupText } from "../../shared/conversation-catchup.js";
|
|
24
25
|
import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
|
|
25
26
|
import {
|
|
26
27
|
formatChannelTemplatesSection,
|
|
@@ -107,6 +108,16 @@ export interface EnhancedPromptOptions {
|
|
|
107
108
|
* store — the context is constant for the session's lifetime.
|
|
108
109
|
*/
|
|
109
110
|
sessionContext?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Conversation catchup (cloud DD-006): what happened on the channel
|
|
113
|
+
* conversation that the agent has not seen, read from the execution
|
|
114
|
+
* spec's `conversation_catchup`. PER-TURN, unlike the three standing
|
|
115
|
+
* siblings above: it rides BOTH prompt paths — this enhanced prompt and
|
|
116
|
+
* a resumed turn's prefix (the `interaction_mode` shape) — because
|
|
117
|
+
* handback lands mid-session on a resumed agent, the exact case the
|
|
118
|
+
* metadata lane cannot reach.
|
|
119
|
+
*/
|
|
120
|
+
conversationCatchup?: string;
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
/**
|
|
@@ -200,6 +211,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
200
211
|
sections.push(formatContextBridgeSection(options.contextBridge));
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
// Catchup after the bridge (DD-007 D-d: bridge first, catchup second) —
|
|
215
|
+
// the bridge carries the pre-takeover conversation, the catchup the human
|
|
216
|
+
// episode, strictly newer by construction; recency puts it closer to the
|
|
217
|
+
// task.
|
|
218
|
+
if (options.conversationCatchup) {
|
|
219
|
+
sections.push(formatConversationCatchupSection(options.conversationCatchup));
|
|
220
|
+
}
|
|
221
|
+
|
|
203
222
|
// Always last before the task: the platform's tool-approval protocol. Placed
|
|
204
223
|
// here for recency so it outweighs any "ask the user first" guidance Cursor
|
|
205
224
|
// surfaces from a connected MCP server (see formatToolApprovalProtocol).
|
|
@@ -345,6 +364,10 @@ export function formatSessionContextSection(context: string): string {
|
|
|
345
364
|
return `<session_context>\n${formatSessionContextText(context)}\n</session_context>`;
|
|
346
365
|
}
|
|
347
366
|
|
|
367
|
+
export function formatConversationCatchupSection(digest: string): string {
|
|
368
|
+
return `<conversation_catchup>\n${formatConversationCatchupText(digest)}\n</conversation_catchup>`;
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
export function formatSkillsSection(skills: SkillMetadata[]): string {
|
|
349
372
|
const entries = skills.map(
|
|
350
373
|
(s) => `- **${s.name}**: ${s.description}\n Path: \`${s.path}\``,
|