@stigmer/runner 3.12.2 → 3.12.4
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-http.js +12 -0
- package/dist/activities/call-http.js.map +1 -1
- package/dist/activities/call-llm.d.ts +18 -0
- package/dist/activities/call-llm.js +56 -2
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
- package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
- package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
- package/dist/activities/execute-cursor/index.js +61 -28
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
- package/dist/activities/execute-cursor/service-tier.js +5 -21
- package/dist/activities/execute-cursor/service-tier.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +15 -0
- package/dist/activities/execute-cursor/skill-resolver.js +45 -7
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +9 -1
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +13 -2
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +14 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
- package/dist/activities/execute-deep-agent/shell-env.js +10 -7
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +5 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js +9 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -1
- package/dist/client/stigmer-client.js +10 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.js +10 -5
- package/dist/config.js.map +1 -1
- package/dist/encryption/config.js +7 -2
- package/dist/encryption/config.js.map +1 -1
- package/dist/main.js +12 -6
- package/dist/main.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -5
- package/dist/middleware/index.js +8 -5
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/tool-intent.d.ts +57 -0
- package/dist/middleware/tool-intent.js +152 -0
- package/dist/middleware/tool-intent.js.map +1 -0
- package/dist/payload-codecs.js +2 -1
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +20 -7
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +19 -6
- package/dist/runner.js.map +1 -1
- package/dist/shared/fingerprint-secret.d.ts +3 -2
- package/dist/shared/fingerprint-secret.js +5 -3
- package/dist/shared/fingerprint-secret.js.map +1 -1
- package/dist/shared/llm-backend.js +8 -1
- package/dist/shared/llm-backend.js.map +1 -1
- package/dist/shared/model-client.d.ts +15 -0
- package/dist/shared/model-client.js +57 -13
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +5 -0
- package/dist/shared/registry-endpoint.js +7 -1
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +26 -1
- package/dist/shared/runner-credential-keys.js +34 -1
- package/dist/shared/runner-credential-keys.js.map +1 -1
- package/dist/shared/runner-credential-store.d.ts +77 -0
- package/dist/shared/runner-credential-store.js +111 -0
- package/dist/shared/runner-credential-store.js.map +1 -0
- package/dist/shared/service-tier.d.ts +55 -0
- package/dist/shared/service-tier.js +67 -0
- package/dist/shared/service-tier.js.map +1 -0
- package/dist/shared/skill-writer.js +2 -2
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/zip-extract.d.ts +10 -3
- package/dist/shared/zip-extract.js +10 -3
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
- package/dist/workflow-engine/tasks/call-function.js +49 -5
- package/dist/workflow-engine/tasks/call-function.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +6 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +36 -8
- package/dist/workflows/engine-core.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/__tests__/call-http.test.ts +36 -0
- package/src/activities/__tests__/call-llm.test.ts +77 -0
- package/src/activities/call-http.ts +17 -0
- package/src/activities/call-llm.ts +78 -2
- package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +104 -1
- package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
- package/src/activities/execute-cursor/index.ts +66 -20
- package/src/activities/execute-cursor/service-tier.ts +5 -29
- package/src/activities/execute-cursor/skill-resolver.ts +52 -8
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +13 -2
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +21 -20
- package/src/activities/execute-deep-agent/setup.ts +15 -0
- package/src/activities/execute-deep-agent/shell-env.ts +10 -7
- package/src/activities/execute-deep-agent/subagent-wiring.ts +10 -1
- package/src/client/stigmer-client.ts +12 -1
- package/src/config.ts +10 -5
- package/src/encryption/config.ts +8 -2
- package/src/main.ts +16 -6
- package/src/middleware/__tests__/tool-intent.test.ts +266 -0
- package/src/middleware/index.ts +9 -5
- package/src/middleware/tool-intent.ts +174 -0
- package/src/payload-codecs.ts +2 -1
- package/src/runner-manager.ts +29 -6
- package/src/runner.ts +25 -6
- package/src/shared/__tests__/model-client.test.ts +99 -0
- package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
- package/src/shared/__tests__/zip-extract.test.ts +46 -11
- package/src/shared/fingerprint-secret.ts +5 -3
- package/src/shared/llm-backend.ts +7 -1
- package/src/shared/model-client.ts +76 -13
- package/src/shared/registry-endpoint.ts +9 -1
- package/src/shared/runner-credential-keys.ts +36 -1
- package/src/shared/runner-credential-store.ts +115 -0
- package/src/shared/service-tier.ts +78 -0
- package/src/shared/skill-writer.ts +2 -2
- package/src/shared/zip-extract.ts +14 -7
- package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
- package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
- package/src/workflow-engine/tasks/call-function.ts +74 -13
- package/src/workflow-engine/types.ts +6 -0
- package/src/workflows/engine-core.ts +39 -8
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-keyed Cursor agent cache — keeps the SDK executor (and its stdio
|
|
3
|
+
* MCP server processes) alive across turns of the SAME session (#215).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: the SDK's local executor cache is refcounted and keyed
|
|
6
|
+
* by the full acquisition config (workingDirectory, hashed apiKey,
|
|
7
|
+
* settingSources, mcpServers, customSubagents). `agent.close()` releases
|
|
8
|
+
* the agent's lease; at refcount zero the executor is DISPOSED and every
|
|
9
|
+
* stdio MCP server is killed. The activity used to close on every terminal
|
|
10
|
+
* path, so each turn re-acquired the executor and re-spawned every MCP
|
|
11
|
+
* server inside `agent.send()` — a measured 2.2–3.2s per-turn tax
|
|
12
|
+
* (`turn_first_event`'s `send_returned` segment).
|
|
13
|
+
*
|
|
14
|
+
* Ownership model — exclusive checkout, explicit lifetime:
|
|
15
|
+
* - A finishing turn PARKS its healthy agent here (`cacheSessionAgent`);
|
|
16
|
+
* the next activity for the session CHECKS IT OUT (`takeCachedAgent`),
|
|
17
|
+
* removing it from the cache, so two concurrent activities can never
|
|
18
|
+
* share one Agent handle — the loser of the race resolves its own.
|
|
19
|
+
* - The cached agent is reused only when the acquisition FINGERPRINT
|
|
20
|
+
* matches. Any config drift (rotated credential, edited MCP servers,
|
|
21
|
+
* model change, different workspace) closes the parked agent and forces
|
|
22
|
+
* a fresh resolve — correctness by construction: a reused executor would
|
|
23
|
+
* otherwise keep serving the OLD config.
|
|
24
|
+
* - Failure paths never park: a suspect agent is closed where it failed
|
|
25
|
+
* (the pre-existing close sites), and `evictSessionAgent` clears any
|
|
26
|
+
* parked entry when a session's state is replaced out from under it.
|
|
27
|
+
* - Idle TTL + LRU cap bound memory on multi-session hosts (the desktop
|
|
28
|
+
* runner-manager hosts many sessions per process; a cloud sandbox is
|
|
29
|
+
* session-pinned and holds at most one entry). Worker shutdown closes
|
|
30
|
+
* everything (`closeAllCachedAgents`).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
|
|
35
|
+
/** The slice of SDKAgent this cache needs — close() releases the executor lease. */
|
|
36
|
+
export interface CacheableAgent {
|
|
37
|
+
readonly agentId: string;
|
|
38
|
+
close(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CachedSessionAgent {
|
|
42
|
+
readonly agent: CacheableAgent;
|
|
43
|
+
readonly fingerprint: string;
|
|
44
|
+
readonly evictTimer: NodeJS.Timeout;
|
|
45
|
+
/** Insertion-order tiebreaker for the LRU cap. */
|
|
46
|
+
readonly parkedAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default idle lifetime for a parked agent. Long enough to cover human
|
|
51
|
+
* think-time between turns and approval round-trips; short enough that an
|
|
52
|
+
* abandoned session does not pin MCP subprocesses for hours.
|
|
53
|
+
*/
|
|
54
|
+
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ceiling on concurrently parked agents (each holds an executor + its MCP
|
|
58
|
+
* subprocesses). Cloud sandboxes never approach it (one session per pod);
|
|
59
|
+
* it protects long-lived desktop runner-managers.
|
|
60
|
+
*/
|
|
61
|
+
const MAX_PARKED_AGENTS = 32;
|
|
62
|
+
|
|
63
|
+
function resolveIdleTtlMs(): number {
|
|
64
|
+
const parsed = Number.parseInt(process.env.STIGMER_CURSOR_AGENT_CACHE_TTL_MS ?? "", 10);
|
|
65
|
+
return parsed > 0 ? parsed : DEFAULT_IDLE_TTL_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parkedAgents = new Map<string, CachedSessionAgent>();
|
|
69
|
+
|
|
70
|
+
function closeQuietly(agent: CacheableAgent): void {
|
|
71
|
+
try {
|
|
72
|
+
agent.close();
|
|
73
|
+
} catch {
|
|
74
|
+
/* best effort — the lease release is advisory on an already-dead agent */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fingerprint of everything that determines whether a parked agent can
|
|
80
|
+
* serve the next turn as-is: the SDK executor cache key inputs PLUS the
|
|
81
|
+
* per-agent options resume() re-supplies (model selection, sub-agents).
|
|
82
|
+
* The API key contributes only as a hash — the fingerprint must never be
|
|
83
|
+
* a secret-bearing value (it appears in no logs, but defense in depth).
|
|
84
|
+
*/
|
|
85
|
+
export function computeAgentFingerprint(createOptions: Record<string, unknown>): string {
|
|
86
|
+
const { apiKey, ...rest } = createOptions;
|
|
87
|
+
const material = {
|
|
88
|
+
...rest,
|
|
89
|
+
apiKeyHash:
|
|
90
|
+
typeof apiKey === "string" && apiKey.length > 0
|
|
91
|
+
? createHash("sha256").update(apiKey).digest("hex")
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
return createHash("sha256").update(stableStringify(material)).digest("hex");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Deterministic JSON: object keys sorted recursively (the SDK's own idiom). */
|
|
98
|
+
function stableStringify(value: unknown): string {
|
|
99
|
+
return JSON.stringify(sortKeys(value));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sortKeys(value: unknown): unknown {
|
|
103
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
104
|
+
if (value !== null && typeof value === "object" && value.constructor === Object) {
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
Object.keys(value as Record<string, unknown>)
|
|
107
|
+
.sort()
|
|
108
|
+
.map((k) => [k, sortKeys((value as Record<string, unknown>)[k])]),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Exclusive checkout: returns the parked agent for the session and removes
|
|
116
|
+
* it from the cache, or undefined when there is nothing reusable.
|
|
117
|
+
*
|
|
118
|
+
* A parked agent is reusable only when BOTH hold:
|
|
119
|
+
* - `fingerprint` matches (config identical to what the agent was built with);
|
|
120
|
+
* - `expectedAgentId`, when non-empty, matches the parked agent (the
|
|
121
|
+
* session's harnessStateId is the source of truth — a recovery in another
|
|
122
|
+
* activity may have replaced the agent since this one was parked).
|
|
123
|
+
*
|
|
124
|
+
* A mismatch on either closes the parked agent: it can never serve this
|
|
125
|
+
* session again, and holding it would only pin dead MCP processes.
|
|
126
|
+
*/
|
|
127
|
+
export function takeCachedAgent(
|
|
128
|
+
sessionId: string,
|
|
129
|
+
fingerprint: string,
|
|
130
|
+
expectedAgentId: string,
|
|
131
|
+
): CacheableAgent | undefined {
|
|
132
|
+
const entry = parkedAgents.get(sessionId);
|
|
133
|
+
if (!entry) return undefined;
|
|
134
|
+
|
|
135
|
+
parkedAgents.delete(sessionId);
|
|
136
|
+
clearTimeout(entry.evictTimer);
|
|
137
|
+
|
|
138
|
+
const agentMatches = expectedAgentId === "" || entry.agent.agentId === expectedAgentId;
|
|
139
|
+
if (entry.fingerprint !== fingerprint || !agentMatches) {
|
|
140
|
+
console.log(
|
|
141
|
+
`agent-session-cache: parked agent for session=${sessionId} not reusable ` +
|
|
142
|
+
`(fingerprintMatch=${entry.fingerprint === fingerprint}, agentIdMatch=${agentMatches}) — closing`,
|
|
143
|
+
);
|
|
144
|
+
closeQuietly(entry.agent);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return entry.agent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parks a healthy agent for the session's next turn. Replaces (and closes)
|
|
153
|
+
* any agent already parked for the session; evicts the oldest entry when
|
|
154
|
+
* the cap is reached.
|
|
155
|
+
*/
|
|
156
|
+
export function cacheSessionAgent(
|
|
157
|
+
sessionId: string,
|
|
158
|
+
agent: CacheableAgent,
|
|
159
|
+
fingerprint: string,
|
|
160
|
+
): void {
|
|
161
|
+
if (!sessionId) {
|
|
162
|
+
// No stable key to reuse by — release the lease as before the cache.
|
|
163
|
+
closeQuietly(agent);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const displaced = parkedAgents.get(sessionId);
|
|
168
|
+
if (displaced) {
|
|
169
|
+
clearTimeout(displaced.evictTimer);
|
|
170
|
+
closeQuietly(displaced.agent);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (parkedAgents.size >= MAX_PARKED_AGENTS) {
|
|
174
|
+
let oldestKey: string | undefined;
|
|
175
|
+
let oldestAt = Infinity;
|
|
176
|
+
for (const [key, entry] of parkedAgents) {
|
|
177
|
+
if (entry.parkedAt < oldestAt) {
|
|
178
|
+
oldestAt = entry.parkedAt;
|
|
179
|
+
oldestKey = key;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (oldestKey !== undefined) evictSessionAgent(oldestKey);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ttlMs = resolveIdleTtlMs();
|
|
186
|
+
const evictTimer = setTimeout(() => evictSessionAgent(sessionId), ttlMs);
|
|
187
|
+
// Never keep the process alive just to evict an idle agent.
|
|
188
|
+
evictTimer.unref?.();
|
|
189
|
+
|
|
190
|
+
parkedAgents.set(sessionId, {
|
|
191
|
+
agent,
|
|
192
|
+
fingerprint,
|
|
193
|
+
evictTimer,
|
|
194
|
+
parkedAt: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Closes and forgets the parked agent for a session, if any. */
|
|
199
|
+
export function evictSessionAgent(sessionId: string): void {
|
|
200
|
+
const entry = parkedAgents.get(sessionId);
|
|
201
|
+
if (!entry) return;
|
|
202
|
+
parkedAgents.delete(sessionId);
|
|
203
|
+
clearTimeout(entry.evictTimer);
|
|
204
|
+
closeQuietly(entry.agent);
|
|
205
|
+
console.log(`agent-session-cache: evicted parked agent for session=${sessionId}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Worker shutdown: release every parked lease so executors dispose cleanly. */
|
|
209
|
+
export function closeAllCachedAgents(): void {
|
|
210
|
+
for (const [sessionId, entry] of parkedAgents) {
|
|
211
|
+
clearTimeout(entry.evictTimer);
|
|
212
|
+
closeQuietly(entry.agent);
|
|
213
|
+
console.log(`agent-session-cache: closed parked agent for session=${sessionId} (shutdown)`);
|
|
214
|
+
}
|
|
215
|
+
parkedAgents.clear();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Test seam. */
|
|
219
|
+
export function _resetAgentSessionCacheForTests(): void {
|
|
220
|
+
for (const entry of parkedAgents.values()) {
|
|
221
|
+
clearTimeout(entry.evictTimer);
|
|
222
|
+
}
|
|
223
|
+
parkedAgents.clear();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Test seam. */
|
|
227
|
+
export function _parkedAgentCountForTests(): number {
|
|
228
|
+
return parkedAgents.size;
|
|
229
|
+
}
|
|
@@ -41,6 +41,7 @@ import type { Config } from "../../config.js";
|
|
|
41
41
|
import { StigmerClient } from "../../client/stigmer-client.js";
|
|
42
42
|
import { describeExecutionError } from "../../shared/model-error.js";
|
|
43
43
|
import { resolveAgentWithTransportRecovery } from "./session-lifecycle.js";
|
|
44
|
+
import { cacheSessionAgent, computeAgentFingerprint, takeCachedAgent } from "./agent-session-cache.js";
|
|
44
45
|
import type { AgentResolution, AgentResolutionReason, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
|
|
45
46
|
import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
46
47
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
@@ -134,7 +135,8 @@ import { statusProtoWriter } from "../../shared/execution-status-writer.js";
|
|
|
134
135
|
import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
|
|
135
136
|
import { closeProxySessions } from "./http2-interceptor.js";
|
|
136
137
|
import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pricing.js";
|
|
137
|
-
import { resolveEffectiveServiceTier
|
|
138
|
+
import { resolveEffectiveServiceTier } from "../../shared/service-tier.js";
|
|
139
|
+
import { resolveServiceTierParams } from "./service-tier.js";
|
|
138
140
|
import { UsageAccumulator } from "./usage-accumulator.js";
|
|
139
141
|
import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
140
142
|
import { activityStarted, activityFinished } from "../../idle-watchdog.js";
|
|
@@ -1052,21 +1054,52 @@ async function executeCursorInner(
|
|
|
1052
1054
|
// is the largest user-visible setup segment; this split keeps its
|
|
1053
1055
|
// historical meaning — the SDK call was already 98%+ of it).
|
|
1054
1056
|
setupTiming.mark("prepare_agent");
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1057
|
+
|
|
1058
|
+
// Phase 8a: Reuse the previous turn's agent when this session parked one
|
|
1059
|
+
// (#215). A checkout hit skips Agent.resume() AND — the real win — keeps
|
|
1060
|
+
// the SDK executor lease alive, so agent.send() below re-acquires the
|
|
1061
|
+
// warm executor instead of re-spawning every stdio MCP server (the
|
|
1062
|
+
// measured 2.2–3.2s `send_returned` tax). The fingerprint covers the
|
|
1063
|
+
// full acquisition config, so any drift (rotated credential, edited MCP
|
|
1064
|
+
// servers, model change) falls through to a fresh resolve.
|
|
1065
|
+
const agentFingerprint = computeAgentFingerprint(
|
|
1066
|
+
createOptions as unknown as Record<string, unknown>,
|
|
1067
|
+
);
|
|
1068
|
+
const parkedAgent = takeCachedAgent(sessionId, agentFingerprint, threadId ?? "");
|
|
1069
|
+
let resolution: AgentResolution;
|
|
1070
|
+
if (parkedAgent) {
|
|
1071
|
+
console.log(
|
|
1072
|
+
`ExecuteCursor reusing parked session agent: execution=${executionId}, ` +
|
|
1073
|
+
`session=${sessionId}, agentId=${parkedAgent.agentId}`,
|
|
1074
|
+
);
|
|
1075
|
+
resolution = {
|
|
1076
|
+
agent: parkedAgent as AgentResolution["agent"],
|
|
1077
|
+
agentId: parkedAgent.agentId,
|
|
1078
|
+
isNew: false,
|
|
1079
|
+
resumed: true,
|
|
1080
|
+
mode: agentMode,
|
|
1081
|
+
// The parked handle IS the live conversation — every consumer of
|
|
1082
|
+
// "resumed_successfully" (prompt selection, poisoned-handle
|
|
1083
|
+
// recovery eligibility) wants exactly those semantics.
|
|
1084
|
+
reason: "resumed_successfully",
|
|
1085
|
+
};
|
|
1086
|
+
} else {
|
|
1087
|
+
resolution = await resolveAgentWithTransportRecovery({
|
|
1088
|
+
harnessStateId: threadId,
|
|
1089
|
+
createOptions,
|
|
1090
|
+
mode: agentMode,
|
|
1091
|
+
timeoutMs: config.agentResolveTimeoutMs,
|
|
1092
|
+
buildTimeoutMessage: (finalAttempt) =>
|
|
1093
|
+
`Cursor agent ${threadId ? "resume" : "create"} timed out after ${resolveTimeoutSeconds}s ` +
|
|
1094
|
+
`(${config.proxyEndpoint ? `via proxy ${config.proxyEndpoint}` : "direct Cursor API connection"}). ` +
|
|
1095
|
+
`The transport connection is likely dead. ` +
|
|
1096
|
+
(finalAttempt
|
|
1097
|
+
? `An automatic retry on a fresh transport connection also timed out. ` +
|
|
1098
|
+
`Retry the message later; if this persists, check proxy and network health.`
|
|
1099
|
+
: `Resetting the transport and retrying automatically.`),
|
|
1100
|
+
resetTransport: closeProxySessions,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1070
1103
|
|
|
1071
1104
|
console.log(
|
|
1072
1105
|
`ExecuteCursor agent resolved: execution=${executionId}, ` +
|
|
@@ -1462,7 +1495,9 @@ async function executeCursorInner(
|
|
|
1462
1495
|
timestamp: utcTimestamp(),
|
|
1463
1496
|
}));
|
|
1464
1497
|
await persist(status);
|
|
1465
|
-
|
|
1498
|
+
// Clean terminal: the conversation continues on the next message,
|
|
1499
|
+
// so park the healthy agent for that turn (#215).
|
|
1500
|
+
cacheSessionAgent(sessionId ?? "", resolution.agent, agentFingerprint);
|
|
1466
1501
|
console.warn(
|
|
1467
1502
|
`ExecuteCursor terminated (cost cap): execution=${executionId}, ` +
|
|
1468
1503
|
`estimatedCostUsd=${estimated.toFixed(4)}, maxCostUsd=${maxCostUsd.toFixed(2)}`,
|
|
@@ -1527,7 +1562,8 @@ async function executeCursorInner(
|
|
|
1527
1562
|
timestamp: utcTimestamp(),
|
|
1528
1563
|
}));
|
|
1529
1564
|
await persist(status);
|
|
1530
|
-
|
|
1565
|
+
// Clean terminal — park for the session's next turn (#215).
|
|
1566
|
+
cacheSessionAgent(sessionId ?? "", resolution.agent, agentFingerprint);
|
|
1531
1567
|
console.log(`ExecuteCursor completed (platform stop): execution=${executionId}`);
|
|
1532
1568
|
return { kind: "return" };
|
|
1533
1569
|
}
|
|
@@ -1573,6 +1609,12 @@ async function executeCursorInner(
|
|
|
1573
1609
|
const enterApprovalPause = async (boundary: TurnBoundaryResult) => {
|
|
1574
1610
|
status.phase = ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL;
|
|
1575
1611
|
await persist(status);
|
|
1612
|
+
// The approval-resume reinvocation is the cache's best case: park the
|
|
1613
|
+
// agent so the resumed turn skips the full executor rebuild (#215).
|
|
1614
|
+
// (This path previously dropped the handle without close() — the
|
|
1615
|
+
// lease leaked; parking makes the lifetime explicit.) An absent
|
|
1616
|
+
// sessionId falls back to "" — the cache closes the lease immediately.
|
|
1617
|
+
cacheSessionAgent(sessionId ?? "", resolution.agent, agentFingerprint);
|
|
1576
1618
|
console.log(
|
|
1577
1619
|
`ExecuteCursor returning WAITING_FOR_APPROVAL: ${boundary.deniedToolCallCount} gated tool(s), ` +
|
|
1578
1620
|
`${boundary.capturedChangeCount} file card(s) pending`,
|
|
@@ -2088,8 +2130,12 @@ async function executeCursorInner(
|
|
|
2088
2130
|
(status.error ? `, error=${status.error}` : ""),
|
|
2089
2131
|
);
|
|
2090
2132
|
|
|
2091
|
-
//
|
|
2092
|
-
|
|
2133
|
+
// Park the agent (with its executor lease) for the session's next turn
|
|
2134
|
+
// instead of closing it — the idle TTL / shutdown hooks in
|
|
2135
|
+
// agent-session-cache own the eventual release, so cache buildup across
|
|
2136
|
+
// sessions stays bounded while turns of ONE session stop paying the
|
|
2137
|
+
// executor + MCP re-spawn tax (#215).
|
|
2138
|
+
cacheSessionAgent(sessionId ?? "", resolution.agent, agentFingerprint);
|
|
2093
2139
|
|
|
2094
2140
|
const slim = slimStatus(status) as Record<string, unknown>;
|
|
2095
2141
|
if (finalText !== undefined) {
|
|
@@ -20,19 +20,17 @@
|
|
|
20
20
|
* rides the same proxy fetch-interceptor as every other SDK call, so it
|
|
21
21
|
* works identically in proxy and direct modes.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* The harness-neutral halves — the tier enum semantics, and the single
|
|
24
|
+
* UNSPECIFIED→STANDARD resolution point — live in
|
|
25
|
+
* `shared/service-tier.ts` since #361 extended tiers to the native
|
|
26
|
+
* harness; this module keeps only the Cursor-catalog translation.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
29
|
import { Cursor } from "@cursor/sdk";
|
|
29
30
|
import type { ModelListItem, ModelParameterValue } from "@cursor/sdk";
|
|
30
31
|
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
* The effective tier after platform-default resolution: never UNSPECIFIED.
|
|
34
|
-
*/
|
|
35
|
-
export type EffectiveServiceTier = ServiceTier.STANDARD | ServiceTier.FAST;
|
|
33
|
+
import { serviceTierLabel, type EffectiveServiceTier } from "../../shared/service-tier.js";
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
36
|
* Catalog ids that mean "Cursor picks the model" (Auto). Auto's single
|
|
@@ -72,28 +70,6 @@ export function resetCatalogCacheForTests(): void {
|
|
|
72
70
|
inflightCatalogFetch = null;
|
|
73
71
|
}
|
|
74
72
|
|
|
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
73
|
async function listCatalogModels(apiKey: string): Promise<readonly ModelListItem[]> {
|
|
98
74
|
const now = Date.now();
|
|
99
75
|
if (catalogCache && catalogCache.apiKey === apiKey && catalogCache.expiresAt > now) {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
22
23
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
23
24
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
24
25
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
@@ -115,14 +116,15 @@ export async function resolveSkills(
|
|
|
115
116
|
let artifactBytes: Uint8Array | undefined;
|
|
116
117
|
if (wantsArtifact) {
|
|
117
118
|
try {
|
|
118
|
-
|
|
119
|
-
if (resp.artifact && resp.artifact.length > 0) {
|
|
120
|
-
artifactBytes = resp.artifact;
|
|
121
|
-
}
|
|
119
|
+
artifactBytes = await downloadArtifact(client, skill.status!.artifactStorageKey);
|
|
122
120
|
} catch (err) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
// Deliberate degradation, but LOUD (#675): the session still gets
|
|
122
|
+
// SKILL.md (better than a dead run), yet a skill silently missing
|
|
123
|
+
// its scripts/references was exactly how oversized artifacts hid.
|
|
124
|
+
console.error(
|
|
125
|
+
`[resolveSkills] artifact download FAILED for ${ref.org || "(default)"}/${ref.slug} ` +
|
|
126
|
+
`(key=${skill.status!.artifactStorageKey}) — mounting SKILL.md WITHOUT the skill's ` +
|
|
127
|
+
`supporting files (scripts/references will be missing): ${err instanceof Error ? err.message : err}`,
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -161,6 +163,48 @@ async function mountIsFresh(skillDir: string, versionHash: string, wantsArtifact
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Download a skill artifact's ZIP bytes, transfer lane first (#675).
|
|
168
|
+
*
|
|
169
|
+
* The URL lane (getArtifactDownloadUrl → HTTP GET) carries any valid skill
|
|
170
|
+
* size; the unary getArtifact response is capped by the server's 10MB gRPC
|
|
171
|
+
* message limit. Servers that predate the lane (and cloud until its sibling
|
|
172
|
+
* lands) answer the mint with UNIMPLEMENTED — those fall back to the unary
|
|
173
|
+
* path, which behaves exactly as before for ≤10MB artifacts.
|
|
174
|
+
*
|
|
175
|
+
* Runs only on a mount-cache miss (#672's hash-keyed marker above) — a hit
|
|
176
|
+
* skips the transfer entirely, whichever lane would have carried it.
|
|
177
|
+
*
|
|
178
|
+
* Exported for tests.
|
|
179
|
+
*/
|
|
180
|
+
export async function downloadArtifact(
|
|
181
|
+
client: StigmerClient,
|
|
182
|
+
artifactStorageKey: string,
|
|
183
|
+
): Promise<Uint8Array | undefined> {
|
|
184
|
+
let minted;
|
|
185
|
+
try {
|
|
186
|
+
minted = await client.getSkillArtifactDownloadUrl(artifactStorageKey);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof ConnectError && err.code === Code.Unimplemented) {
|
|
189
|
+
const resp = await client.getSkillArtifact(artifactStorageKey);
|
|
190
|
+
return resp.artifact && resp.artifact.length > 0 ? resp.artifact : undefined;
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const resp = await fetch(minted.url);
|
|
196
|
+
if (!resp.ok) {
|
|
197
|
+
throw new Error(`artifact fetch failed: HTTP ${resp.status} from ${minted.url}`);
|
|
198
|
+
}
|
|
199
|
+
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
200
|
+
if (minted.sizeBytes > 0n && BigInt(bytes.length) !== minted.sizeBytes) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`artifact fetch truncated: got ${bytes.length} bytes, expected ${minted.sizeBytes}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return bytes.length > 0 ? bytes : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
/**
|
|
165
209
|
* (Re)write a skill's mount directory from scratch.
|
|
166
210
|
*
|
|
@@ -188,7 +232,7 @@ async function writeSkillMount(
|
|
|
188
232
|
for (const entry of entries) {
|
|
189
233
|
const filePath = join(skillDir, entry.path);
|
|
190
234
|
await mkdir(dirname(filePath), { recursive: true });
|
|
191
|
-
await writeFile(filePath, entry.content
|
|
235
|
+
await writeFile(filePath, entry.content);
|
|
192
236
|
}
|
|
193
237
|
}
|
|
194
238
|
|
|
@@ -50,11 +50,20 @@ export type ScriptSelector = (boundToolNames: string[]) => ScriptStep;
|
|
|
50
50
|
*/
|
|
51
51
|
export class ScriptedModel extends BaseChatModel {
|
|
52
52
|
toolNames: string[] = [];
|
|
53
|
+
/**
|
|
54
|
+
* The tool objects from the most recent `bindTools` call, exactly as the
|
|
55
|
+
* agent bound them (post-middleware). Lets tests assert on the bound
|
|
56
|
+
* SCHEMAS — e.g. the tool-intent middleware's bind-time shell clone — not
|
|
57
|
+
* just the names. The array is shared across the clones `bindTools`
|
|
58
|
+
* returns, so the instance the test holds always sees the latest bind.
|
|
59
|
+
*/
|
|
60
|
+
readonly boundTools: unknown[];
|
|
53
61
|
private readonly select: ScriptSelector;
|
|
54
62
|
|
|
55
|
-
constructor(select: ScriptSelector) {
|
|
63
|
+
constructor(select: ScriptSelector, boundTools: unknown[] = []) {
|
|
56
64
|
super({});
|
|
57
65
|
this.select = select;
|
|
66
|
+
this.boundTools = boundTools;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
_llmType(): string {
|
|
@@ -62,8 +71,10 @@ export class ScriptedModel extends BaseChatModel {
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
bindTools(tools: unknown[]): this {
|
|
65
|
-
const next = new ScriptedModel(this.select);
|
|
74
|
+
const next = new ScriptedModel(this.select, this.boundTools);
|
|
66
75
|
next.toolNames = (tools as Array<{ name?: string }>).map((t) => t?.name ?? "");
|
|
76
|
+
this.boundTools.length = 0;
|
|
77
|
+
this.boundTools.push(...tools);
|
|
67
78
|
return next as unknown as this;
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -7,11 +7,12 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
7
7
|
it("returns the correct middleware order without cost cap", () => {
|
|
8
8
|
const stack = buildSubAgentMiddleware();
|
|
9
9
|
|
|
10
|
-
expect(stack).toHaveLength(
|
|
10
|
+
expect(stack).toHaveLength(5);
|
|
11
11
|
expect(stack[0].name).toBe("LoopDetectionMiddleware");
|
|
12
12
|
expect(stack[1].name).toBe("ExecutionBudgetMiddleware");
|
|
13
|
-
expect(stack[2].name).toBe("
|
|
14
|
-
expect(stack[3].name).toBe("
|
|
13
|
+
expect(stack[2].name).toBe("StigmerToolIntentMiddleware");
|
|
14
|
+
expect(stack[3].name).toBe("ToolTruncationMiddleware");
|
|
15
|
+
expect(stack[4].name).toBe("ErrorHintsMiddleware");
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
it("includes cost cap view when parent cost cap is provided", () => {
|
|
@@ -25,9 +26,9 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
25
26
|
|
|
26
27
|
const stack = buildSubAgentMiddleware({ costCap: parentCostCap });
|
|
27
28
|
|
|
28
|
-
expect(stack).toHaveLength(
|
|
29
|
-
expect(stack[
|
|
30
|
-
expect(stack[
|
|
29
|
+
expect(stack).toHaveLength(6);
|
|
30
|
+
expect(stack[4].name).toBe("CostCapSubAgentView");
|
|
31
|
+
expect(stack[5].name).toBe("ErrorHintsMiddleware");
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it("sub-agent cost cap view shares parent state", () => {
|
|
@@ -43,7 +44,7 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
43
44
|
|
|
44
45
|
expect(parentCostCap.runningCost).toBe(0);
|
|
45
46
|
|
|
46
|
-
const subView = stack[
|
|
47
|
+
const subView = stack[4];
|
|
47
48
|
expect(subView.afterModel).toBeDefined();
|
|
48
49
|
expect(subView.wrapToolCall).toBeDefined();
|
|
49
50
|
expect(subView.beforeAgent).toBeUndefined();
|
|
@@ -69,7 +70,7 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
69
70
|
toolTruncation: { maxChars: 5000 },
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
expect(stack[
|
|
73
|
+
expect(stack[3].name).toBe("ToolTruncationMiddleware");
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
it("installs the approval gate when an approvalGate config is provided", () => {
|
|
@@ -77,16 +78,16 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
77
78
|
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
78
79
|
});
|
|
79
80
|
|
|
80
|
-
// loop, budget, truncation, approval gate, error hints
|
|
81
|
-
expect(stack).toHaveLength(
|
|
82
|
-
expect(stack[
|
|
83
|
-
expect(stack[
|
|
81
|
+
// loop, budget, tool intent, truncation, approval gate, error hints
|
|
82
|
+
expect(stack).toHaveLength(6);
|
|
83
|
+
expect(stack[4].name).toBe("ApprovalGateMiddleware");
|
|
84
|
+
expect(stack[4].wrapToolCall).toBeDefined();
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
it("omits the approval gate when approvalGate is null (auto-approve-all parity)", () => {
|
|
87
88
|
const stack = buildSubAgentMiddleware({ approvalGate: null });
|
|
88
89
|
|
|
89
|
-
expect(stack).toHaveLength(
|
|
90
|
+
expect(stack).toHaveLength(5);
|
|
90
91
|
expect(stack.some((m) => m.name === "ApprovalGateMiddleware")).toBe(false);
|
|
91
92
|
});
|
|
92
93
|
|
|
@@ -104,13 +105,13 @@ describe("buildSubAgentMiddleware", () => {
|
|
|
104
105
|
approvalGate: { policies: new Map(), toolServerMap: new Map() },
|
|
105
106
|
});
|
|
106
107
|
|
|
107
|
-
// loop, budget, truncation, approval gate, cost cap view,
|
|
108
|
-
// Hints AFTER the gate matches the parent nesting: the
|
|
109
|
-
// interrupt stays outside the hints' try/catch (issue #255).
|
|
110
|
-
expect(stack).toHaveLength(
|
|
111
|
-
expect(stack[
|
|
112
|
-
expect(stack[
|
|
113
|
-
expect(stack[
|
|
108
|
+
// loop, budget, tool intent, truncation, approval gate, cost cap view,
|
|
109
|
+
// error hints. Hints AFTER the gate matches the parent nesting: the
|
|
110
|
+
// gate's HITL interrupt stays outside the hints' try/catch (issue #255).
|
|
111
|
+
expect(stack).toHaveLength(7);
|
|
112
|
+
expect(stack[4].name).toBe("ApprovalGateMiddleware");
|
|
113
|
+
expect(stack[5].name).toBe("CostCapSubAgentView");
|
|
114
|
+
expect(stack[6].name).toBe("ErrorHintsMiddleware");
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
// captureIgnored is the structural coupling that makes sub-agent gitignored
|
|
@@ -72,6 +72,7 @@ import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
|
|
|
72
72
|
import { getModelPricing, ensureLoaded as ensurePricingLoaded } from "../../shared/model-pricing.js";
|
|
73
73
|
import { getDefaultModel, getModelVisionCapability } from "../../shared/model-registry.js";
|
|
74
74
|
import { buildChatModel } from "../../shared/model-client.js";
|
|
75
|
+
import { resolveEffectiveServiceTier } from "../../shared/service-tier.js";
|
|
75
76
|
import {
|
|
76
77
|
loadArtifactStorageConfig,
|
|
77
78
|
resolveUsableArtifactStorage,
|
|
@@ -580,11 +581,22 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
580
581
|
// The operator's STIGMER_LLM_REQUEST_TIMEOUT_MS bound is applied inside
|
|
581
582
|
// buildChatModel (#468) — the sub-agent modelFactory below silently
|
|
582
583
|
// dropped it when each caller parsed the env itself.
|
|
584
|
+
//
|
|
585
|
+
// The service tier resolves ONCE here (UNSPECIFIED → explicit
|
|
586
|
+
// STANDARD, shared/service-tier.ts) and rides every model this
|
|
587
|
+
// execution constructs — the primary below AND the sub-agent factory —
|
|
588
|
+
// so the provider account's default can never pick the price of any
|
|
589
|
+
// turn (#361, the native half of #357's contract). Sub-agents inherit
|
|
590
|
+
// it because the tier is an attribute of the EXECUTION's bill, and
|
|
591
|
+
// sub-agent calls land on the same ledger.
|
|
592
|
+
const serviceTier = resolveEffectiveServiceTier(
|
|
593
|
+
execution.spec!.executionConfig?.serviceTier);
|
|
583
594
|
const { model } = await buildChatModel({
|
|
584
595
|
modelName,
|
|
585
596
|
proxyEndpoint: config.proxyEndpoint ?? undefined,
|
|
586
597
|
stigmerToken: config.stigmerToken ?? undefined,
|
|
587
598
|
headerScope: { executionId },
|
|
599
|
+
serviceTier,
|
|
588
600
|
});
|
|
589
601
|
timing.mark("build_model");
|
|
590
602
|
|
|
@@ -776,6 +788,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
776
788
|
proxyEndpoint: config.proxyEndpoint ?? undefined,
|
|
777
789
|
stigmerToken: config.stigmerToken ?? undefined,
|
|
778
790
|
headerScope: { executionId },
|
|
791
|
+
// The execution's tier, inherited: sub-agent calls bill to the
|
|
792
|
+
// same execution (#361 — see the Step 9 resolution comment).
|
|
793
|
+
serviceTier,
|
|
779
794
|
})).model,
|
|
780
795
|
// Presence of shellEnv is the shell-capability switch for sub-agent
|
|
781
796
|
// backends too (undefined in plan mode; see buildShellEnv above).
|