@stigmer/runner 3.4.1 → 3.5.1
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/attach-session.d.ts +34 -0
- package/dist/activities/attach-session.js +95 -0
- package/dist/activities/attach-session.js.map +1 -0
- package/dist/activities/discover-mcp-server.d.ts +9 -0
- package/dist/activities/discover-mcp-server.js +6 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/connect-backfill.d.ts +2 -1
- package/dist/activities/execute-cursor/connect-backfill.js +2 -2
- package/dist/activities/execute-cursor/connect-backfill.js.map +1 -1
- package/dist/activities/execute-cursor/fetch-interceptor.js +31 -0
- package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +70 -3
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +7 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +15 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/sdk-warmup.d.ts +44 -0
- package/dist/activities/execute-cursor/sdk-warmup.js +61 -0
- package/dist/activities/execute-cursor/sdk-warmup.js.map +1 -0
- package/dist/activities/execute-deep-agent/setup.js +48 -5
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +9 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +19 -5
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +13 -1
- package/dist/client/stigmer-client.js +40 -5
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/client/token-claims.d.ts +9 -6
- package/dist/client/token-claims.js +22 -6
- package/dist/client/token-claims.js.map +1 -1
- package/dist/main.d.ts +8 -1
- package/dist/main.js +122 -2
- package/dist/main.js.map +1 -1
- package/dist/middleware/index.d.ts +0 -1
- package/dist/middleware/index.js +0 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/otel-metrics.d.ts +10 -1
- package/dist/otel-metrics.js +22 -1
- package/dist/otel-metrics.js.map +1 -1
- package/dist/pool-member.d.ts +61 -0
- package/dist/pool-member.js +51 -0
- package/dist/pool-member.js.map +1 -0
- package/dist/runner-manager.d.ts +7 -0
- package/dist/runner-manager.js +39 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +15 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/cold-start-timing.d.ts +89 -0
- package/dist/shared/cold-start-timing.js +166 -0
- package/dist/shared/cold-start-timing.js.map +1 -0
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +4 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +5 -15
- package/dist/shared/mcp-manager.js +5 -32
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +15 -5
- package/dist/shared/mcp-resolver.js +23 -6
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/mcp-transport-guard.d.ts +53 -0
- package/dist/shared/mcp-transport-guard.js +70 -0
- package/dist/shared/mcp-transport-guard.js.map +1 -0
- package/dist/shared/tool-kind.js +6 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/tools/index.d.ts +27 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/think-tool.js.map +1 -0
- package/dist/tools/url-guard.d.ts +64 -0
- package/dist/tools/url-guard.js +211 -0
- package/dist/tools/url-guard.js.map +1 -0
- package/dist/tools/web-fetch-tool.d.ts +53 -0
- package/dist/tools/web-fetch-tool.js +216 -0
- package/dist/tools/web-fetch-tool.js.map +1 -0
- package/package.json +4 -2
- package/src/__tests__/otel-metrics.test.ts +6 -0
- package/src/__tests__/pool-member.test.ts +76 -0
- package/src/activities/__tests__/attach-session.test.ts +133 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +34 -11
- package/src/activities/attach-session.ts +112 -0
- package/src/activities/discover-mcp-server.ts +19 -1
- package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +77 -0
- package/src/activities/execute-cursor/__tests__/sdk-warmup.test.ts +45 -0
- package/src/activities/execute-cursor/connect-backfill.ts +3 -0
- package/src/activities/execute-cursor/fetch-interceptor.ts +39 -0
- package/src/activities/execute-cursor/index.ts +72 -3
- package/src/activities/execute-cursor/mcp-resolver.ts +21 -2
- package/src/activities/execute-cursor/sdk-warmup.ts +68 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +24 -0
- package/src/activities/execute-deep-agent/setup.ts +48 -7
- package/src/activities/execute-deep-agent/subagent-transformer.ts +27 -2
- package/src/client/__tests__/stigmer-client.test.ts +77 -2
- package/src/client/__tests__/token-claims.test.ts +15 -1
- package/src/client/stigmer-client.ts +51 -6
- package/src/client/token-claims.ts +29 -6
- package/src/main.ts +139 -2
- package/src/middleware/index.ts +0 -1
- package/src/otel-metrics.ts +25 -1
- package/src/pool-member.ts +99 -0
- package/src/runner-manager.ts +52 -0
- package/src/runner.ts +15 -0
- package/src/shared/__tests__/cold-start-timing.test.ts +271 -0
- package/src/shared/__tests__/connect-backfill.test.ts +11 -11
- package/src/shared/__tests__/mcp-manager.test.ts +2 -73
- package/src/shared/__tests__/mcp-resolver.test.ts +108 -0
- package/src/shared/__tests__/mcp-transport-guard.test.ts +71 -0
- package/src/shared/cold-start-timing.ts +197 -0
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-manager.ts +4 -41
- package/src/shared/mcp-resolver.ts +29 -6
- package/src/shared/mcp-transport-guard.ts +81 -0
- package/src/shared/tool-kind.ts +6 -0
- package/src/tools/__tests__/url-guard.test.ts +137 -0
- package/src/tools/__tests__/web-fetch-tool.test.ts +193 -0
- package/src/tools/index.ts +28 -0
- package/src/tools/url-guard.ts +232 -0
- package/src/tools/web-fetch-tool.ts +275 -0
- package/dist/middleware/think-tool.js.map +0 -1
- /package/dist/{middleware → tools}/think-tool.d.ts +0 -0
- /package/dist/{middleware → tools}/think-tool.js +0 -0
- /package/src/{middleware → tools}/__tests__/think-tool.test.ts +0 -0
- /package/src/{middleware → tools}/think-tool.ts +0 -0
|
@@ -45,6 +45,7 @@ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_p
|
|
|
45
45
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
46
46
|
import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
|
|
47
47
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
48
|
+
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
48
49
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
49
50
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
50
51
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
@@ -57,6 +58,7 @@ import { TodoTracker } from "./todo-tracker.js";
|
|
|
57
58
|
import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
58
59
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
59
60
|
import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
61
|
+
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
60
62
|
import {
|
|
61
63
|
injectDatastoreAttachment,
|
|
62
64
|
synthesizeDatastoreAttachment,
|
|
@@ -189,6 +191,12 @@ async function executeCursorInner(
|
|
|
189
191
|
startedAt: utcTimestamp(),
|
|
190
192
|
});
|
|
191
193
|
|
|
194
|
+
// Cold-start timeline of this turn's setup (warm-agent-surfaces Phase 0):
|
|
195
|
+
// one mark after each phase, emitted as a single structured log line once
|
|
196
|
+
// the Cursor agent is resolved (early returns skip it — partial setups are
|
|
197
|
+
// not comparable cold-start samples).
|
|
198
|
+
const setupTiming = new TimingRecorder();
|
|
199
|
+
|
|
192
200
|
// Artifact storage for offloading oversized tool outputs (screenshots, giant
|
|
193
201
|
// dumps) out of the persisted status, and for publishing the plan artifact.
|
|
194
202
|
// Resolved once here so it is available to EVERY persist below. Best-effort via
|
|
@@ -198,6 +206,7 @@ async function executeCursorInner(
|
|
|
198
206
|
// the aggregate size cap) and flips capture mode off (deny-gate fallback).
|
|
199
207
|
const artifactStorage: ArtifactStorage | undefined =
|
|
200
208
|
await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
|
|
209
|
+
setupTiming.mark("resolve_artifact_storage");
|
|
201
210
|
const statusOffload = artifactStorage
|
|
202
211
|
? { artifactStorage, executionId }
|
|
203
212
|
: undefined;
|
|
@@ -289,17 +298,20 @@ async function executeCursorInner(
|
|
|
289
298
|
const execution = await client.getExecution(executionId);
|
|
290
299
|
const spec = execution.spec!;
|
|
291
300
|
sessionId = spec.sessionId;
|
|
301
|
+
setupTiming.mark("fetch_execution");
|
|
292
302
|
|
|
293
303
|
// Phase 2: Load session and resolve full agent blueprint
|
|
294
304
|
await reportSetupProgress(client, executionId, "Resolving agent blueprint");
|
|
295
305
|
session = await client.getSession(sessionId);
|
|
296
306
|
const blueprint = await resolveBlueprint(client, session, config.workspaceRootDir);
|
|
307
|
+
setupTiming.mark("resolve_blueprint");
|
|
297
308
|
|
|
298
309
|
// Phase 2b: Resolve execution environment (MCP server credentials)
|
|
299
310
|
heartbeatPhase = "resolving_environment";
|
|
300
311
|
await reportSetupProgress(client, executionId, "Resolving environment");
|
|
301
312
|
const { envVars, secretKeys } = await resolveExecutionEnv(client, executionId);
|
|
302
313
|
heartbeat();
|
|
314
|
+
setupTiming.mark("resolve_environment");
|
|
303
315
|
|
|
304
316
|
// Phase 2c: Provision the workspace (clone git repos / mount local paths)
|
|
305
317
|
// so the LOCAL Cursor agent operates on the actual repo. Cursor previously
|
|
@@ -314,6 +326,7 @@ async function executeCursorInner(
|
|
|
314
326
|
);
|
|
315
327
|
blueprint.workspaceDirs = workspaceProvision.workspaceDirs;
|
|
316
328
|
heartbeat();
|
|
329
|
+
setupTiming.mark("provision_workspace");
|
|
317
330
|
|
|
318
331
|
// Git write-back: pushes the session's APPROVED tree to the session
|
|
319
332
|
// branch (stigmer/<session-id>) and keeps one PR open — the same
|
|
@@ -404,6 +417,7 @@ async function executeCursorInner(
|
|
|
404
417
|
}
|
|
405
418
|
}
|
|
406
419
|
heartbeat();
|
|
420
|
+
setupTiming.mark("acquire_workspace_lock");
|
|
407
421
|
|
|
408
422
|
// Set OTel baggage so downstream calls carry execution context.
|
|
409
423
|
try {
|
|
@@ -577,17 +591,20 @@ async function executeCursorInner(
|
|
|
577
591
|
|
|
578
592
|
// Phase 4: Resolve MCP servers with approval policies
|
|
579
593
|
await reportSetupProgress(client, executionId, "Resolving MCP servers");
|
|
594
|
+
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
580
595
|
let mcpResolution = await resolveMcpServers(
|
|
581
|
-
client, blueprint.mergedMcpServerUsages, envVars,
|
|
596
|
+
client, blueprint.mergedMcpServerUsages, envVars, transportPosture,
|
|
582
597
|
);
|
|
598
|
+
setupTiming.mark("resolve_mcp_servers");
|
|
583
599
|
|
|
584
600
|
// Phase 4a: Connect backfill for undiscovered MCP servers
|
|
585
601
|
heartbeatPhase = "resolving_mcp_servers";
|
|
586
602
|
const sessionOrg = session.metadata?.org ?? "";
|
|
587
603
|
mcpResolution = await backfillMcpServersIfNeeded(
|
|
588
604
|
client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
|
|
589
|
-
heartbeat, secretKeys,
|
|
605
|
+
transportPosture, heartbeat, secretKeys,
|
|
590
606
|
);
|
|
607
|
+
setupTiming.mark("backfill_mcp");
|
|
591
608
|
|
|
592
609
|
// Phase 4a2: Synthesize the datastore records attachment (T05).
|
|
593
610
|
// Deliberately AFTER resolve + backfill: the attachment has no
|
|
@@ -657,6 +674,7 @@ async function executeCursorInner(
|
|
|
657
674
|
primaryWorkspaceDir,
|
|
658
675
|
});
|
|
659
676
|
heartbeat();
|
|
677
|
+
setupTiming.mark("resolve_skills");
|
|
660
678
|
|
|
661
679
|
// Phase 5b: Resolve attachments (fail-hard — explicit user inputs; see
|
|
662
680
|
// attachment-resolver.ts). Downloads by storage key through the same
|
|
@@ -668,6 +686,7 @@ async function executeCursorInner(
|
|
|
668
686
|
storage: artifactStorage,
|
|
669
687
|
});
|
|
670
688
|
const attachmentPaths = attachmentResults.map((a) => a.relativePath);
|
|
689
|
+
setupTiming.mark("resolve_attachments");
|
|
671
690
|
|
|
672
691
|
// Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
|
|
673
692
|
// is what gets applied"). The Cursor deny-only harness reinvokes the model,
|
|
@@ -797,6 +816,7 @@ async function executeCursorInner(
|
|
|
797
816
|
stopDenialWatcher = watchDenialLedger(hitlDir, () => {
|
|
798
817
|
turnState.denialLedgerDirty = true;
|
|
799
818
|
});
|
|
819
|
+
setupTiming.mark("install_hitl_gate");
|
|
800
820
|
|
|
801
821
|
// Mid-run live capture (DD-32 / DD-33): choose the progress substrate for this
|
|
802
822
|
// turn's workspace shape ONCE (git / non-git CAS / hybrid). It owns its own
|
|
@@ -815,6 +835,7 @@ async function executeCursorInner(
|
|
|
815
835
|
|
|
816
836
|
// Phase 5d: Ensure model pricing registry is populated before validation
|
|
817
837
|
await ensurePricingLoaded();
|
|
838
|
+
setupTiming.mark("load_pricing");
|
|
818
839
|
|
|
819
840
|
// Phase 6: Validate model selection
|
|
820
841
|
const requestedModel = spec.executionConfig?.modelName || "default";
|
|
@@ -887,6 +908,12 @@ async function executeCursorInner(
|
|
|
887
908
|
// the activity returns rather than throws, and maximumAttempts is 1).
|
|
888
909
|
heartbeatPhase = "resolving_agent";
|
|
889
910
|
const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
|
|
911
|
+
// Close the span since load_pricing here so the resolve_agent segment
|
|
912
|
+
// below measures the SDK Agent.create/resume call alone, not the
|
|
913
|
+
// progress-report gRPC + options assembly above (issue #209: resolve_agent
|
|
914
|
+
// is the largest user-visible setup segment; this split keeps its
|
|
915
|
+
// historical meaning — the SDK call was already 98%+ of it).
|
|
916
|
+
setupTiming.mark("prepare_agent");
|
|
890
917
|
let resolution: AgentResolution = await resolveAgentWithTransportRecovery({
|
|
891
918
|
harnessStateId: threadId,
|
|
892
919
|
createOptions,
|
|
@@ -909,6 +936,17 @@ async function executeCursorInner(
|
|
|
909
936
|
`agentId=${resolution.agentId}, resumed=${resolution.resumed}` +
|
|
910
937
|
(resolution.resumeFailureDetail ? `, failureDetail=${resolution.resumeFailureDetail}` : ""),
|
|
911
938
|
);
|
|
939
|
+
setupTiming.mark("resolve_agent");
|
|
940
|
+
emitTimingLog("execution_setup", {
|
|
941
|
+
execution_id: executionId,
|
|
942
|
+
session_id: sessionId,
|
|
943
|
+
harness: "cursor",
|
|
944
|
+
agent_resumed: resolution.resumed,
|
|
945
|
+
cursor_mode: agentMode,
|
|
946
|
+
mcp_server_count: blueprint.mergedMcpServerUsages.length,
|
|
947
|
+
skill_count: blueprint.mergedSkillRefs.length,
|
|
948
|
+
workspace_entry_count: session.spec?.workspaceEntries?.length ?? 0,
|
|
949
|
+
}, setupTiming);
|
|
912
950
|
|
|
913
951
|
errorContext = { model: validatedModel, mode: agentMode, agentId: resolution.agentId };
|
|
914
952
|
|
|
@@ -1068,12 +1106,43 @@ async function executeCursorInner(
|
|
|
1068
1106
|
// (e.g. older SDK), the warning is harmless — ignore.
|
|
1069
1107
|
}
|
|
1070
1108
|
|
|
1109
|
+
// Issue #209 forensics: the SDK acquires the local executor — the piece
|
|
1110
|
+
// that actually spawns stdio MCP servers — inside send(), AFTER the
|
|
1111
|
+
// execution_setup timeline above has already been emitted. This one-shot
|
|
1112
|
+
// timeline makes that previously invisible window measurable:
|
|
1113
|
+
// `send_returned` covers the send() call itself, `first_delta` the wait
|
|
1114
|
+
// until the SDK's first delta. Primary send only — the recovery retries
|
|
1115
|
+
// below rebuild the agent and would skew the user-perceived turn start
|
|
1116
|
+
// this measures. No delta (immediate pause/failure) → no line.
|
|
1117
|
+
const turnStartTiming = new TimingRecorder();
|
|
1118
|
+
let turnFirstEventEmitted = false;
|
|
1119
|
+
const primaryOnDelta = makeCursorTurnOnDelta(onDeltaDeps);
|
|
1120
|
+
|
|
1071
1121
|
// The stall watchdog is armed inside consumeCursorTurnStream (it needs the
|
|
1072
1122
|
// run to cancel), stored on turnState.stallWatchdog so this shared onDelta can
|
|
1073
1123
|
// reset it and the activity's finally can stop it as a backstop.
|
|
1074
1124
|
const run = await resolution.agent.send(effectivePrompt, {
|
|
1075
|
-
onDelta:
|
|
1125
|
+
onDelta: (event) => {
|
|
1126
|
+
if (!turnFirstEventEmitted) {
|
|
1127
|
+
turnFirstEventEmitted = true;
|
|
1128
|
+
turnStartTiming.mark("first_delta");
|
|
1129
|
+
emitTimingLog("turn_first_event", {
|
|
1130
|
+
execution_id: executionId,
|
|
1131
|
+
session_id: sessionId,
|
|
1132
|
+
harness: "cursor",
|
|
1133
|
+
agent_resumed: resolution.resumed,
|
|
1134
|
+
mcp_server_count: blueprint.mergedMcpServerUsages.length,
|
|
1135
|
+
}, turnStartTiming);
|
|
1136
|
+
}
|
|
1137
|
+
primaryOnDelta(event);
|
|
1138
|
+
},
|
|
1076
1139
|
});
|
|
1140
|
+
// Normally send() resolves before any delta arrives, making send_returned
|
|
1141
|
+
// the first segment; if a delta beat it, the line is already emitted and
|
|
1142
|
+
// adding a mark now would be meaningless.
|
|
1143
|
+
if (!turnFirstEventEmitted) {
|
|
1144
|
+
turnStartTiming.mark("send_returned");
|
|
1145
|
+
}
|
|
1077
1146
|
|
|
1078
1147
|
// Everything at an index >= this was produced by THIS turn's stream — the
|
|
1079
1148
|
// positional turn boundary the approved-command provenance (DD-28) scopes
|
|
@@ -18,6 +18,11 @@ import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1
|
|
|
18
18
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
19
19
|
import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
20
20
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
21
|
+
import {
|
|
22
|
+
assertTransportAllowed,
|
|
23
|
+
McpTransportError,
|
|
24
|
+
type McpTransportPosture,
|
|
25
|
+
} from "../../shared/mcp-transport-guard.js";
|
|
21
26
|
import {
|
|
22
27
|
resolveHeaders,
|
|
23
28
|
resolvePlaceholders,
|
|
@@ -85,11 +90,17 @@ export interface McpResolutionResult {
|
|
|
85
90
|
* 2. Extract connection config (stdio or http) from McpServerSpec
|
|
86
91
|
* 3. Extract approval policies from McpServerStatus + McpServerSpec
|
|
87
92
|
* 4. Transform into Cursor SDK's mcpServers config
|
|
93
|
+
*
|
|
94
|
+
* @param transportPosture Whether stdio servers may run here (derive via
|
|
95
|
+
* resolveMcpTransportPosture(config.mode)). A stdio server under a
|
|
96
|
+
* forbidding posture throws {@link McpTransportError} and fails the
|
|
97
|
+
* whole resolution — never degraded to a skipped server.
|
|
88
98
|
*/
|
|
89
99
|
export async function resolveMcpServers(
|
|
90
100
|
client: StigmerClient,
|
|
91
101
|
usages: McpServerUsage[],
|
|
92
|
-
envVars: Record<string, string
|
|
102
|
+
envVars: Record<string, string>,
|
|
103
|
+
transportPosture: McpTransportPosture,
|
|
93
104
|
): Promise<McpResolutionResult> {
|
|
94
105
|
const resolved: ResolvedMcpServer[] = [];
|
|
95
106
|
|
|
@@ -105,8 +116,16 @@ export async function resolveMcpServers(
|
|
|
105
116
|
ref.slug,
|
|
106
117
|
);
|
|
107
118
|
const server = mcpServerToResolved(mcpServer, ref.slug, serverEnv);
|
|
108
|
-
if (server)
|
|
119
|
+
if (server) {
|
|
120
|
+
assertTransportAllowed(server.slug, server.connectionType, transportPosture);
|
|
121
|
+
resolved.push(server);
|
|
122
|
+
}
|
|
109
123
|
} catch (err) {
|
|
124
|
+
if (err instanceof McpTransportError) {
|
|
125
|
+
// Policy rejection, not a resolution hiccup: swallowing it here
|
|
126
|
+
// would mean the agent silently loses tools. Fail the execution.
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
110
129
|
if (err instanceof PlaceholderResolutionError) {
|
|
111
130
|
console.error(
|
|
112
131
|
`MCP server ${ref.org}/${ref.slug}: ${err.message}`,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor SDK state-store warm-up for pool members.
|
|
3
|
+
*
|
|
4
|
+
* The first `Agent.create` in a pod pays the SDK's platform construction:
|
|
5
|
+
* four SQLite-backed stores (run/checkpoint/event stores + notifier) whose
|
|
6
|
+
* first use loads the sqlite native binding and compiles the schema
|
|
7
|
+
* migrations. The 2026-07-30 instrumented baseline (issue #209) measured
|
|
8
|
+
* this at ~1.2s of the ~2.4s `resolve_agent` segment, with a 624ms floor —
|
|
9
|
+
* roughly half the cost is per-process warm-up, not per-session work.
|
|
10
|
+
*
|
|
11
|
+
* Pool members idle between boot and claim, so constructing one throwaway
|
|
12
|
+
* platform there moves that per-process cost off the user-visible path.
|
|
13
|
+
* This respects the pool's pre-warm boundary (warm-agent-surfaces DD-C:
|
|
14
|
+
* image caches only, nothing per-agent): the throwaway store is
|
|
15
|
+
* org-agnostic, credential-free, and keyed to a synthetic workspace ref.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT run on session-mode boots: those pods receive their
|
|
18
|
+
* execution the moment the worker polls, so a concurrent warm-up would
|
|
19
|
+
* compete with the real `Agent.create` for the same CPU instead of running
|
|
20
|
+
* in idle time.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { mkdtempSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
export interface SdkWarmupResult {
|
|
28
|
+
readonly warmed: boolean;
|
|
29
|
+
readonly durationMs: number;
|
|
30
|
+
readonly error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Construct (and abandon) one SDK agent platform on a throwaway temp-dir
|
|
35
|
+
* state root, forcing the sqlite native binding + store schema work to
|
|
36
|
+
* happen now instead of inside the first user-facing `Agent.create`.
|
|
37
|
+
*
|
|
38
|
+
* Total by construction — warm-up is an optimization and must never affect
|
|
39
|
+
* the member's ability to serve. The throwaway state root is a few KB on
|
|
40
|
+
* the pod's ephemeral disk and is intentionally NOT deleted: the SDK memoizes
|
|
41
|
+
* the platform (with open SQLite handles) for the process lifetime, and
|
|
42
|
+
* unlinking files under live handles buys nothing on a pod that vanishes
|
|
43
|
+
* with its disk anyway.
|
|
44
|
+
*
|
|
45
|
+
* The `@cursor/sdk` import is dynamic so this module never adds the SDK
|
|
46
|
+
* bundle to any boot path that doesn't already carry it — by the time the
|
|
47
|
+
* warm-up runs (after the worker starts polling), the activities have
|
|
48
|
+
* already loaded the SDK and the import resolves from the module cache.
|
|
49
|
+
*/
|
|
50
|
+
export async function warmCursorSdkStateStores(): Promise<SdkWarmupResult> {
|
|
51
|
+
const startMs = performance.now();
|
|
52
|
+
try {
|
|
53
|
+
const stateRoot = mkdtempSync(join(tmpdir(), "cursor-sdk-warm-"));
|
|
54
|
+
const { createAgentPlatform } = await import("@cursor/sdk");
|
|
55
|
+
await createAgentPlatform({ workspaceRef: "stigmer-warm:boot", stateRoot });
|
|
56
|
+
return { warmed: true, durationMs: elapsed(startMs) };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
warmed: false,
|
|
60
|
+
durationMs: elapsed(startMs),
|
|
61
|
+
error: err instanceof Error ? err.message : String(err),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function elapsed(startMs: number): number {
|
|
67
|
+
return Math.round((performance.now() - startMs) * 10) / 10;
|
|
68
|
+
}
|
|
@@ -121,6 +121,14 @@ describe("createBuiltinSubagents", () => {
|
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it("gives web_fetch to general-purpose only, when a guard posture is supplied", () => {
|
|
125
|
+
const result = createBuiltinSubagents(true, [], "strict");
|
|
126
|
+
for (const sa of result) {
|
|
127
|
+
const hasWebFetch = sa.tools.some((t) => t.name === "web_fetch");
|
|
128
|
+
expect(hasWebFetch).toBe(sa.name === "general-purpose");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
124
132
|
it("built-in subagents have no model override", () => {
|
|
125
133
|
const result = createBuiltinSubagents(true);
|
|
126
134
|
for (const sa of result) {
|
|
@@ -140,6 +148,7 @@ describe("transformSingleSubagent", () => {
|
|
|
140
148
|
parentMcpUsages: [] as McpServerUsage[],
|
|
141
149
|
parentHasNativeThinking: true,
|
|
142
150
|
parentModelName: "claude-sonnet-4-6",
|
|
151
|
+
webFetchPosture: "strict" as const,
|
|
143
152
|
};
|
|
144
153
|
|
|
145
154
|
beforeEach(() => {
|
|
@@ -202,6 +211,17 @@ describe("transformSingleSubagent", () => {
|
|
|
202
211
|
expect(hasThinkTool).toBe(false);
|
|
203
212
|
});
|
|
204
213
|
|
|
214
|
+
it("always injects web_fetch, regardless of thinking support", async () => {
|
|
215
|
+
for (const parentHasNativeThinking of [true, false]) {
|
|
216
|
+
const result = await transformSingleSubagent(mockSubAgentProto(), {
|
|
217
|
+
...baseOpts,
|
|
218
|
+
parentHasNativeThinking,
|
|
219
|
+
});
|
|
220
|
+
const hasWebFetch = result!.tools.some((t) => t.name === "web_fetch");
|
|
221
|
+
expect(hasWebFetch).toBe(true);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
205
225
|
it("injects think tool when parent lacks native thinking", async () => {
|
|
206
226
|
const proto = mockSubAgentProto();
|
|
207
227
|
const result = await transformSingleSubagent(proto, {
|
|
@@ -433,6 +453,7 @@ describe("transformAndCompileSubagents", () => {
|
|
|
433
453
|
approvalGate: null,
|
|
434
454
|
parentModelName: "claude-sonnet-4-6",
|
|
435
455
|
parentHasNativeThinking: true,
|
|
456
|
+
webFetchPosture: "strict" as const,
|
|
436
457
|
costCap: undefined,
|
|
437
458
|
};
|
|
438
459
|
|
|
@@ -639,6 +660,7 @@ describe("subagent-transformer edge cases", () => {
|
|
|
639
660
|
parentMcpUsages: [],
|
|
640
661
|
parentHasNativeThinking: true,
|
|
641
662
|
parentModelName: "claude-sonnet-4-6",
|
|
663
|
+
webFetchPosture: "strict",
|
|
642
664
|
});
|
|
643
665
|
|
|
644
666
|
expect(result).not.toBeNull();
|
|
@@ -664,6 +686,7 @@ describe("subagent-transformer edge cases", () => {
|
|
|
664
686
|
approvalGate: null,
|
|
665
687
|
parentModelName: "claude-sonnet-4-6",
|
|
666
688
|
parentHasNativeThinking: true,
|
|
689
|
+
webFetchPosture: "strict",
|
|
667
690
|
costCap: undefined,
|
|
668
691
|
} as Parameters<typeof transformAndCompileSubagents>[0]);
|
|
669
692
|
|
|
@@ -687,6 +710,7 @@ describe("subagent-transformer edge cases", () => {
|
|
|
687
710
|
approvalGate: null,
|
|
688
711
|
parentModelName: "claude-sonnet-4-6",
|
|
689
712
|
parentHasNativeThinking: true,
|
|
713
|
+
webFetchPosture: "strict",
|
|
690
714
|
costCap: undefined,
|
|
691
715
|
} as Parameters<typeof transformAndCompileSubagents>[0]);
|
|
692
716
|
|
|
@@ -22,12 +22,14 @@ import type { DynamicStructuredTool } from "@langchain/core/tools";
|
|
|
22
22
|
|
|
23
23
|
import type { Config } from "../../config.js";
|
|
24
24
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
25
|
+
import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
|
|
25
26
|
import { createCheckpointer } from "../../shared/checkpointer/factory.js";
|
|
26
27
|
import { readContextBridge } from "../../shared/context-bridge.js";
|
|
27
28
|
import { readSenderIdentity } from "../../shared/sender-identity.js";
|
|
28
29
|
import { readSessionContext } from "../../shared/session-context.js";
|
|
29
30
|
import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
|
|
30
31
|
import { resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
32
|
+
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
31
33
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
32
34
|
import {
|
|
33
35
|
formatDatastoresSection,
|
|
@@ -49,8 +51,9 @@ import { buildWorkspaceFileTree } from "../../shared/workspace/file-tree.js";
|
|
|
49
51
|
import { reportSetupProgress } from "../../shared/status.js";
|
|
50
52
|
import { resolveEnvironment, type EnvironmentResult } from "./environment.js";
|
|
51
53
|
import { buildEnhancedSystemPrompt } from "./prompt-builder.js";
|
|
52
|
-
import { buildMiddlewareStack
|
|
54
|
+
import { buildMiddlewareStack } from "../../middleware/index.js";
|
|
53
55
|
import type { GracefulStopMiddleware } from "../../middleware/index.js";
|
|
56
|
+
import { createThinkTool, createWebFetchTool, resolveGuardPosture } from "../../tools/index.js";
|
|
54
57
|
import type { ApprovalGateConfig } from "../../middleware/approval-gate.js";
|
|
55
58
|
import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
|
|
56
59
|
import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
|
|
@@ -194,12 +197,16 @@ export interface SetupDependencies {
|
|
|
194
197
|
export async function performSetup(deps: SetupDependencies): Promise<SetupResult> {
|
|
195
198
|
const { config, client, executionId, threadId } = deps;
|
|
196
199
|
let mcpConnection: McpConnectionResult | null = null;
|
|
200
|
+
// Cold-start timeline of this setup (warm-agent-surfaces Phase 0): one mark
|
|
201
|
+
// after each phase, emitted as a single structured log line before return.
|
|
202
|
+
const timing = new TimingRecorder();
|
|
197
203
|
|
|
198
204
|
try {
|
|
199
205
|
// Step 1: Hydrate execution
|
|
200
206
|
await reportSetupProgress(client, executionId, "Fetching execution…");
|
|
201
207
|
const execution = await client.getExecution(executionId);
|
|
202
208
|
console.log(`[setup] Execution fetched: agent_id=${execution.spec?.agentId}`);
|
|
209
|
+
timing.mark("fetch_execution");
|
|
203
210
|
|
|
204
211
|
// Step 2: Resolve chain — execution → session → agentInstance → agent
|
|
205
212
|
await reportSetupProgress(client, executionId, "Resolving agent…");
|
|
@@ -220,6 +227,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
220
227
|
`[setup] Chain resolved: session=${sessionId}, ` +
|
|
221
228
|
`agent=${agent.metadata!.name}`,
|
|
222
229
|
);
|
|
230
|
+
timing.mark("resolve_chain");
|
|
223
231
|
|
|
224
232
|
// Step 3: Resolve model
|
|
225
233
|
const modelName = execution.spec!.executionConfig?.modelName
|
|
@@ -237,10 +245,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
237
245
|
? await ensureCheckpointDbPath(sessionId)
|
|
238
246
|
: undefined,
|
|
239
247
|
});
|
|
248
|
+
timing.mark("create_checkpointer");
|
|
240
249
|
|
|
241
250
|
// Step 5: Resolve environment
|
|
242
251
|
await reportSetupProgress(client, executionId, "Resolving environment…");
|
|
243
252
|
const envResult: EnvironmentResult = await resolveEnvironment(client, executionId);
|
|
253
|
+
timing.mark("resolve_environment");
|
|
244
254
|
|
|
245
255
|
// Step 6: Resolve a usable artifact store (shared with the Cursor harness so
|
|
246
256
|
// both degrade identically). Returns `undefined` — never throws — when there
|
|
@@ -250,6 +260,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
250
260
|
// disables offload, instead of flowing writes then crashing at the boundary.
|
|
251
261
|
const artifactStorage: ArtifactStorage | undefined =
|
|
252
262
|
await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
|
|
263
|
+
timing.mark("resolve_artifact_storage");
|
|
253
264
|
|
|
254
265
|
// Step 7: Provision workspace
|
|
255
266
|
await reportSetupProgress(client, executionId, "Initializing workspace…");
|
|
@@ -259,6 +270,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
259
270
|
envResult.mergedEnvVars,
|
|
260
271
|
sessionId,
|
|
261
272
|
);
|
|
273
|
+
timing.mark("provision_workspace");
|
|
262
274
|
|
|
263
275
|
// Apply-then-review is the file-review model whenever there is a capture
|
|
264
276
|
// SUBSTRATE (DD-21 D2, Slice 2b): file edits flow during the turn and are
|
|
@@ -325,9 +337,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
325
337
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
326
338
|
if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
|
|
327
339
|
await reportSetupProgress(client, executionId, "Connecting tools…");
|
|
340
|
+
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
328
341
|
resolvedMcpServers = await resolveMcpServers(
|
|
329
|
-
client, mcpServerUsages, envResult.mergedEnvVars,
|
|
342
|
+
client, mcpServerUsages, envResult.mergedEnvVars, transportPosture,
|
|
330
343
|
);
|
|
344
|
+
timing.mark("resolve_mcp_servers");
|
|
331
345
|
|
|
332
346
|
const sessionOrg = session.metadata?.org ?? "";
|
|
333
347
|
let backfilledServers = await backfillMcpServersIfNeeded(
|
|
@@ -336,6 +350,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
336
350
|
mcpServerUsages,
|
|
337
351
|
envResult.mergedEnvVars,
|
|
338
352
|
sessionOrg,
|
|
353
|
+
transportPosture,
|
|
339
354
|
undefined,
|
|
340
355
|
envResult.secretKeys,
|
|
341
356
|
);
|
|
@@ -359,11 +374,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
359
374
|
}
|
|
360
375
|
}
|
|
361
376
|
resolvedMcpServers = { resolvedServers: backfilledServers };
|
|
377
|
+
timing.mark("backfill_mcp");
|
|
362
378
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
);
|
|
379
|
+
// connect_mcp covers the whole fan-out: stdio servers spawn npx/uvx
|
|
380
|
+
// subprocesses here, so on-demand package installs land in this span.
|
|
381
|
+
mcpConnection = await connectMcpServers(resolvedMcpServers.resolvedServers);
|
|
382
|
+
timing.mark("connect_mcp");
|
|
367
383
|
}
|
|
368
384
|
|
|
369
385
|
// Step 7b: Resolve and write skills
|
|
@@ -407,6 +423,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
407
423
|
`[setup] Skills loaded: ${skills.length} total, prompt section ${skillsPromptSection.length} chars`,
|
|
408
424
|
);
|
|
409
425
|
}
|
|
426
|
+
timing.mark("resolve_skills");
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
// Step 7c: Inject attachments
|
|
@@ -417,6 +434,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
417
434
|
storage: artifactStorage,
|
|
418
435
|
isLocalMode: config.mode === "local",
|
|
419
436
|
});
|
|
437
|
+
timing.mark("inject_attachments");
|
|
420
438
|
|
|
421
439
|
// Step 8: Build enhanced system prompt
|
|
422
440
|
const systemPrompt = buildEnhancedSystemPrompt({
|
|
@@ -449,6 +467,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
449
467
|
headerScope: { executionId },
|
|
450
468
|
timeoutMs: requestTimeoutMs,
|
|
451
469
|
});
|
|
470
|
+
timing.mark("build_model");
|
|
452
471
|
|
|
453
472
|
// Step 10: Build middleware stack
|
|
454
473
|
await ensurePricingLoaded();
|
|
@@ -563,11 +582,22 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
563
582
|
otelSpans: { toolServerMap },
|
|
564
583
|
approvalGate: approvalGateConfig,
|
|
565
584
|
});
|
|
585
|
+
timing.mark("build_middleware");
|
|
566
586
|
|
|
567
|
-
// Step 11: Build tools list (MCP tools +
|
|
587
|
+
// Step 11: Build tools list (MCP tools + native built-in tools).
|
|
588
|
+
//
|
|
589
|
+
// web_fetch is always-on for every native run — the same posture as the
|
|
590
|
+
// Cursor harness, whose WebFetch is a CLI built-in that no configuration
|
|
591
|
+
// removes (issue #214 parity). It is auto-approved by design (see
|
|
592
|
+
// toolApprovalCategory in shared/tool-kind.ts), so its URL guard posture
|
|
593
|
+
// — strict on managed cloud runners, relaxed on user-owned machines — is
|
|
594
|
+
// the entire safety boundary. Derived from config.mode, NOT the
|
|
595
|
+
// cloudModeEnabled Cursor feature flag.
|
|
596
|
+
const webFetchPosture = resolveGuardPosture(config.mode);
|
|
568
597
|
const tools = [
|
|
569
598
|
...(mcpConnection?.tools as DynamicStructuredTool[] ?? []),
|
|
570
599
|
createThinkTool(),
|
|
600
|
+
createWebFetchTool({ posture: webFetchPosture }),
|
|
571
601
|
];
|
|
572
602
|
|
|
573
603
|
// Step 11b: Transform and compile subagents
|
|
@@ -599,6 +629,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
599
629
|
casObserver,
|
|
600
630
|
parentModelName: modelName,
|
|
601
631
|
parentHasNativeThinking: _modelHasNativeThinking(modelName),
|
|
632
|
+
webFetchPosture,
|
|
602
633
|
costCap: costCapMiddleware ?? undefined,
|
|
603
634
|
modelFactory: async (m: string) =>
|
|
604
635
|
(await buildChatModel({
|
|
@@ -611,6 +642,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
611
642
|
// backends too (undefined in plan mode; see buildShellEnv above).
|
|
612
643
|
shellEnv,
|
|
613
644
|
});
|
|
645
|
+
timing.mark("compile_subagents");
|
|
614
646
|
}
|
|
615
647
|
|
|
616
648
|
// Step 12: Create the agent graph with middleware
|
|
@@ -682,6 +714,15 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
682
714
|
`tools=${tools.length}, middleware=${middleware.length}, ` +
|
|
683
715
|
`thread_id=${threadId}, streamVersion=${streamVersion}`,
|
|
684
716
|
);
|
|
717
|
+
timing.mark("create_agent_graph");
|
|
718
|
+
emitTimingLog("execution_setup", {
|
|
719
|
+
execution_id: executionId,
|
|
720
|
+
session_id: sessionId,
|
|
721
|
+
harness: "native",
|
|
722
|
+
mcp_server_count: mcpServerUsages.length,
|
|
723
|
+
skill_count: skillRefs.length,
|
|
724
|
+
workspace_entry_count: session.spec!.workspaceEntries?.length ?? 0,
|
|
725
|
+
}, timing);
|
|
685
726
|
|
|
686
727
|
return {
|
|
687
728
|
agentGraph,
|
|
@@ -33,7 +33,7 @@ import type { ApprovalGateConfig } from "../../middleware/approval-gate.js";
|
|
|
33
33
|
import { createCasCaptureBackend } from "./cas-capture-backend.js";
|
|
34
34
|
import type { CasCaptureObserver } from "./cas-capture-observer.js";
|
|
35
35
|
import type { CostCapMiddleware, StigmerMiddleware } from "../../middleware/index.js";
|
|
36
|
-
import { createThinkTool } from "../../
|
|
36
|
+
import { createThinkTool, createWebFetchTool, type GuardPosture } from "../../tools/index.js";
|
|
37
37
|
import { buildSubAgentMiddleware } from "./subagent-wiring.js";
|
|
38
38
|
import { SubAgentGate } from "../../shared/subagent-gate.js";
|
|
39
39
|
import { isModelRegistered } from "../../shared/model-registry.js";
|
|
@@ -158,6 +158,12 @@ export interface SubagentTransformOptions {
|
|
|
158
158
|
readonly casObserver?: CasCaptureObserver;
|
|
159
159
|
readonly parentModelName: string;
|
|
160
160
|
readonly parentHasNativeThinking: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* URL-guard posture for the native `web_fetch` tool, inherited from the
|
|
163
|
+
* parent (resolveGuardPosture(config.mode) in setup.ts) so a sub-agent
|
|
164
|
+
* fetch is bounded exactly like a parent fetch.
|
|
165
|
+
*/
|
|
166
|
+
readonly webFetchPosture: GuardPosture;
|
|
161
167
|
readonly costCap?: CostCapMiddleware;
|
|
162
168
|
/**
|
|
163
169
|
* Builds a configured chat-model instance for a given model name. When
|
|
@@ -203,6 +209,7 @@ export interface SubagentTransformOptions {
|
|
|
203
209
|
export function createBuiltinSubagents(
|
|
204
210
|
hasWorkspace: boolean,
|
|
205
211
|
parentMcpTools: readonly StructuredTool[] = [],
|
|
212
|
+
webFetchPosture?: GuardPosture,
|
|
206
213
|
): TransformedSubagent[] {
|
|
207
214
|
if (!hasWorkspace) {
|
|
208
215
|
return [];
|
|
@@ -216,7 +223,15 @@ export function createBuiltinSubagents(
|
|
|
216
223
|
name: "general-purpose",
|
|
217
224
|
description: DEFAULT_GENERAL_PURPOSE_DESCRIPTION,
|
|
218
225
|
systemPrompt: DEFAULT_SUBAGENT_PROMPT + RESPONSE_RULES,
|
|
219
|
-
|
|
226
|
+
// Parent-parity tool set: MCP tools plus the native web_fetch the
|
|
227
|
+
// parent always carries. explore/shell stay web-less on purpose —
|
|
228
|
+
// their prompts scope them to the workspace, not the internet.
|
|
229
|
+
tools: [
|
|
230
|
+
...parentMcpTools,
|
|
231
|
+
...(webFetchPosture
|
|
232
|
+
? [createWebFetchTool({ posture: webFetchPosture }) as unknown as StructuredTool]
|
|
233
|
+
: []),
|
|
234
|
+
],
|
|
220
235
|
});
|
|
221
236
|
continue;
|
|
222
237
|
}
|
|
@@ -257,6 +272,7 @@ export async function transformSingleSubagent(
|
|
|
257
272
|
readonly parentMcpUsages: readonly McpServerUsage[];
|
|
258
273
|
readonly parentHasNativeThinking: boolean;
|
|
259
274
|
readonly parentModelName: string;
|
|
275
|
+
readonly webFetchPosture: GuardPosture;
|
|
260
276
|
},
|
|
261
277
|
): Promise<TransformedSubagent | null> {
|
|
262
278
|
const name = subAgent.name;
|
|
@@ -290,6 +306,12 @@ export async function transformSingleSubagent(
|
|
|
290
306
|
tools.push(createThinkTool() as unknown as StructuredTool);
|
|
291
307
|
}
|
|
292
308
|
|
|
309
|
+
// web_fetch is unconditional — unlike think, it is a capability, not a
|
|
310
|
+
// reasoning aid, and the parent always has it (setup.ts Step 11). A
|
|
311
|
+
// sub-agent silently lacking web access the parent has would recreate the
|
|
312
|
+
// harness-parity gap of issue #214 one level down.
|
|
313
|
+
tools.push(createWebFetchTool({ posture: opts.webFetchPosture }) as unknown as StructuredTool);
|
|
314
|
+
|
|
293
315
|
// Append response rules
|
|
294
316
|
systemPrompt += RESPONSE_RULES;
|
|
295
317
|
|
|
@@ -597,6 +619,7 @@ export async function transformAndCompileSubagents(
|
|
|
597
619
|
casObserver,
|
|
598
620
|
parentModelName,
|
|
599
621
|
parentHasNativeThinking,
|
|
622
|
+
webFetchPosture,
|
|
600
623
|
costCap,
|
|
601
624
|
modelFactory,
|
|
602
625
|
shellEnv,
|
|
@@ -615,6 +638,7 @@ export async function transformAndCompileSubagents(
|
|
|
615
638
|
const builtins = createBuiltinSubagents(
|
|
616
639
|
!!workspaceBackend.rootDir,
|
|
617
640
|
parentMcpTools,
|
|
641
|
+
webFetchPosture,
|
|
618
642
|
);
|
|
619
643
|
|
|
620
644
|
// Step 1b: Batch fetch all skills referenced by subagents
|
|
@@ -665,6 +689,7 @@ export async function transformAndCompileSubagents(
|
|
|
665
689
|
parentMcpUsages,
|
|
666
690
|
parentHasNativeThinking,
|
|
667
691
|
parentModelName,
|
|
692
|
+
webFetchPosture,
|
|
668
693
|
});
|
|
669
694
|
|
|
670
695
|
if (result) {
|