@yanlinglabs/winter-agent-runtime 0.0.27 → 0.0.28

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.
@@ -12,6 +12,7 @@ import {
12
12
  getRegisteredTool,
13
13
  listRegisteredTools,
14
14
  rebrandStandingServerTools,
15
+ mcpServerHasRegistrations,
15
16
  STRUCTURED_OUTPUT_TOOL_NAME,
16
17
  looksLikeCommand,
17
18
  registerChildEngineFactory,
@@ -86,7 +87,7 @@ import {
86
87
  providerCredentialRef2,
87
88
  createModelClassifier2,
88
89
  selectClassifierRoute2
89
- } from "./index-9qgkpv56.js";
90
+ } from "./index-mftq279y.js";
90
91
  import {
91
92
  createInMemoryChannel2
92
93
  } from "./index-97t2rmtf.js";
@@ -473,6 +474,64 @@ async function cleanupWorkspace(workspace) {
473
474
  }
474
475
 
475
476
  // src/subagents/child-engine.ts
477
+ var liveChildScopedServerNames = new Set;
478
+ var GENERATION_TEARDOWN_WAIT_MS = 5000;
479
+ function renameMcpReference(entry, renames) {
480
+ for (const [declared, actual] of renames) {
481
+ const prefix = `mcp__${declared}`;
482
+ if (entry === prefix || entry.startsWith(`${prefix}__`) || entry.startsWith(`${prefix}(`))
483
+ return `mcp__${actual}${entry.slice(prefix.length)}`;
484
+ }
485
+ return;
486
+ }
487
+ function allocateChildScopedServers(declared, parentVisible, prefer, parentDeclared) {
488
+ const claimed = [];
489
+ const inUse = (name) => liveChildScopedServerNames.has(name) || parentVisible.has(name) || mcpServerHasRegistrations(name);
490
+ const servers = {};
491
+ const actual = new Map;
492
+ const notes = [];
493
+ const claim = (name) => {
494
+ liveChildScopedServerNames.add(name);
495
+ claimed.push(name);
496
+ };
497
+ for (const [name, cfg] of Object.entries(declared)) {
498
+ if (cfg.type === "sdk") {
499
+ if (inUse(name)) {
500
+ notes.push(parentVisible.has(name) && parentDeclared?.[name]?.type === "sdk" ? `mcpServers declares the in-process server "${name}", which this session already has -- the session's "${name}" is used` : parentVisible.has(name) ? `mcpServers declares the in-process server "${name}", but this session's "${name}" is a different server -- this agent's own in-process "${name}" is not connected, and the session's is what this agent sees` : `mcpServers declares the in-process server "${name}", which another agent in this session is using -- this agent's own in-process "${name}" is not connected`);
501
+ continue;
502
+ }
503
+ claim(name);
504
+ servers[name] = cfg;
505
+ actual.set(name, name);
506
+ continue;
507
+ }
508
+ const free = (candidate) => !inUse(candidate) && (candidate === name || !(candidate in declared));
509
+ let pick = prefer?.get(name);
510
+ if (pick === undefined || !free(pick))
511
+ pick = free(name) ? name : undefined;
512
+ for (let n = 2;pick === undefined; n++)
513
+ if (free(`${name}_${n}`))
514
+ pick = `${name}_${n}`;
515
+ claim(pick);
516
+ servers[pick] = cfg;
517
+ actual.set(name, pick);
518
+ if (pick !== name)
519
+ notes.push(`mcpServers declares "${name}", which is already in use in this session -- this agent's own "${name}" is connected as "${pick}" (its tools are named mcp__${pick}__<tool>)`);
520
+ }
521
+ let released = false;
522
+ return {
523
+ servers,
524
+ actual,
525
+ notes,
526
+ release: () => {
527
+ if (released)
528
+ return;
529
+ released = true;
530
+ for (const name of claimed)
531
+ liveChildScopedServerNames.delete(name);
532
+ }
533
+ };
534
+ }
476
535
  function createChildEngineFactory(deps) {
477
536
  return (runCtx) => ({
478
537
  spawn(req, inherit) {
@@ -487,8 +546,20 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
487
546
  const env = deps.env ?? process.env;
488
547
  const { depth: spawnDepth } = checkAndRegisterSpawn({ parentKey: runCtx.parentAgentId ?? runCtx.parentSessionId, childKey: agentId, env, ...deps.parentBrand !== undefined ? { brand: deps.parentBrand } : {} });
489
548
  let spawnRegistered = true;
549
+ let pendingServerRelease;
490
550
  try {
491
- let startGeneration = function(config, initialMessages, liveText, agentSystemPrompt) {
551
+ let startGeneration = function(config, initialMessages, liveText, agentSystemPrompt, servers) {
552
+ const ownServers = Object.keys(servers.servers).length > 0;
553
+ if (ownServers)
554
+ config = { ...config, mcpServers: servers.servers };
555
+ const renames = [...servers.actual].filter(([declared, actual]) => declared !== actual);
556
+ if (renames.length > 0 && config.disallowedTools !== undefined) {
557
+ const carried = config.disallowedTools.flatMap((entry) => {
558
+ const renamed = renameMcpReference(entry, renames);
559
+ return renamed !== undefined ? [entry, renamed] : [entry];
560
+ });
561
+ config = { ...config, disallowedTools: [...new Set(carried)] };
562
+ }
492
563
  const channel = createInMemoryChannel2();
493
564
  currentSink = channel.host.output;
494
565
  const startedAt = Date.now();
@@ -675,48 +746,63 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
675
746
  }
676
747
  };
677
748
  })();
678
- runEngine2({
679
- config,
680
- contextAccountant: childAccountant,
681
- ...childProvider.identity !== undefined ? { providerIdentity: childProvider.identity } : {},
682
- input: channel.runtime.input,
683
- output: snapshottingOutput,
684
- provider: childProvider.provider,
685
- ...agentSystemPrompt !== undefined && agentSystemPrompt.length > 0 ? { agentSystemPrompt } : {},
686
- ...req.fork === true && inherit.requestLayout !== undefined ? { exactRequestLayout: inherit.requestLayout } : {},
687
- subagentHooks: {
688
- agentType: req.agentType ?? req.builtinAgentType ?? (req.fork === true ? "fork" : "general-purpose"),
689
- agentTranscriptPath: childStore !== undefined && childDurableRoot !== undefined ? transcriptPath : ""
690
- },
691
- ...req.definition?.omitProjectContext === true ? { omitProjectContext: true } : {},
692
- ...writer !== undefined ? { store: writer } : {},
693
- ...initialMessages.length > 0 ? { initialMessages } : {},
694
- ...!hasChildScopedMcpServers && parentMcp?.stateSource !== undefined ? { mcpServerStateSource: parentMcp.stateSource } : {},
695
- ...!hasChildScopedMcpServers && parentMcp?.controlSeam !== undefined ? { mcpControlSeam: parentMcp.controlSeam } : {},
696
- ...parentMcp?.visibleServerNames !== undefined ? { inheritedMcpServerNames: parentMcp.visibleServerNames } : {},
697
- ...deps.systemPromptAssembler !== undefined ? { systemPromptAssembler: deps.systemPromptAssembler } : {},
698
- ...deps.skillListing !== undefined ? { skillListing: deps.skillListing } : {},
699
- ...deps.describeModel !== undefined ? { describeModel: deps.describeModel } : {},
700
- ...deps.priceUsage !== undefined ? {
701
- priceUsage: (modelKey, usage) => deps.priceUsage(modelKey === resolvedModel.effectiveModel && childProvider.identity !== undefined ? childProvider.identity.modelKey : modelKey, usage)
702
- } : {},
703
- ...deps.usageRowFacts !== undefined ? {
704
- usageRowFacts: (modelKey) => deps.usageRowFacts(modelKey === resolvedModel.effectiveModel && childProvider.identity !== undefined ? childProvider.identity.modelKey : modelKey)
705
- } : {},
706
- ...runCtx.recordDescendantCost !== undefined ? { onPricedGeneration: runCtx.recordDescendantCost } : {},
707
- ...runCtx.budgetExceeded !== undefined ? { ancestorBudgetExceeded: () => runCtx.budgetExceeded() } : {},
708
- ...deps.resolveAuxiliaryModel !== undefined ? { resolveAuxiliaryModel: deps.resolveAuxiliaryModel } : {},
709
- ...deps.resolveToolSecret !== undefined ? { resolveToolSecret: deps.resolveToolSecret } : {},
710
- ...deps.settingsRules !== undefined ? { settingsRules: deps.settingsRules } : {},
711
- ...childDurableRoot !== undefined ? { winterHome: childDurableRoot } : {},
712
- ...deps.structuredOutput !== undefined ? { structuredOutput: deps.structuredOutput } : {},
713
- ...deps.extraHookEntries !== undefined ? { extraHookEntries: deps.extraHookEntries } : {},
714
- ...ownCompactionController !== undefined ? { compactionController: ownCompactionController } : {},
715
- env
716
- }).catch((err) => {
717
- const reason = err instanceof Error && err.message.length > 0 ? err.message : String(err ?? "");
718
- settle("failed", reason.length > 0 ? `child engine exited: ${reason}` : "child engine process exited unexpectedly");
719
- });
749
+ let handedOver = false;
750
+ try {
751
+ lastGenerationDone = runEngine2({
752
+ config,
753
+ contextAccountant: childAccountant,
754
+ ...childProvider.identity !== undefined ? { providerIdentity: childProvider.identity } : {},
755
+ input: channel.runtime.input,
756
+ output: snapshottingOutput,
757
+ provider: childProvider.provider,
758
+ ...agentSystemPrompt !== undefined && agentSystemPrompt.length > 0 ? { agentSystemPrompt } : {},
759
+ ...req.fork === true && inherit.requestLayout !== undefined ? { exactRequestLayout: inherit.requestLayout } : {},
760
+ subagentHooks: {
761
+ agentType: req.agentType ?? req.builtinAgentType ?? (req.fork === true ? "fork" : "general-purpose"),
762
+ agentTranscriptPath: childStore !== undefined && childDurableRoot !== undefined ? transcriptPath : ""
763
+ },
764
+ ...req.definition?.omitProjectContext === true ? { omitProjectContext: true } : {},
765
+ ...writer !== undefined ? { store: writer } : {},
766
+ ...initialMessages.length > 0 ? { initialMessages } : {},
767
+ ...!ownServers && parentMcp?.stateSource !== undefined ? { mcpServerStateSource: parentMcp.stateSource } : {},
768
+ ...!ownServers && parentMcp?.controlSeam !== undefined ? { mcpControlSeam: parentMcp.controlSeam } : {},
769
+ ...(() => {
770
+ const merged = { ...parentMcp?.serverRenames ?? {}, ...Object.fromEntries(renames.map(([declared, actual]) => [actual, declared])) };
771
+ return Object.keys(merged).length > 0 ? { mcpServerRenames: merged } : {};
772
+ })(),
773
+ ...parentMcp?.visibleServerNames !== undefined ? { inheritedMcpServerNames: ownServerNames.size === 0 ? parentMcp.visibleServerNames : () => parentMcp.visibleServerNames().filter((name) => !ownServerNames.has(name)) } : {},
774
+ ...deps.systemPromptAssembler !== undefined ? { systemPromptAssembler: deps.systemPromptAssembler } : {},
775
+ ...deps.skillListing !== undefined ? { skillListing: deps.skillListing } : {},
776
+ ...deps.describeModel !== undefined ? { describeModel: deps.describeModel } : {},
777
+ ...deps.priceUsage !== undefined ? {
778
+ priceUsage: (modelKey, usage) => deps.priceUsage(modelKey === resolvedModel.effectiveModel && childProvider.identity !== undefined ? childProvider.identity.modelKey : modelKey, usage)
779
+ } : {},
780
+ ...deps.usageRowFacts !== undefined ? {
781
+ usageRowFacts: (modelKey) => deps.usageRowFacts(modelKey === resolvedModel.effectiveModel && childProvider.identity !== undefined ? childProvider.identity.modelKey : modelKey)
782
+ } : {},
783
+ ...runCtx.recordDescendantCost !== undefined ? { onPricedGeneration: runCtx.recordDescendantCost } : {},
784
+ ...runCtx.budgetExceeded !== undefined ? { ancestorBudgetExceeded: () => runCtx.budgetExceeded() } : {},
785
+ ...deps.resolveAuxiliaryModel !== undefined ? { resolveAuxiliaryModel: deps.resolveAuxiliaryModel } : {},
786
+ ...deps.resolveToolSecret !== undefined ? { resolveToolSecret: deps.resolveToolSecret } : {},
787
+ ...deps.settingsRules !== undefined ? { settingsRules: deps.settingsRules } : {},
788
+ ...childDurableRoot !== undefined ? { winterHome: childDurableRoot } : {},
789
+ ...deps.structuredOutput !== undefined ? { structuredOutput: deps.structuredOutput } : {},
790
+ ...deps.extraHookEntries !== undefined ? { extraHookEntries: deps.extraHookEntries } : {},
791
+ ...ownCompactionController !== undefined ? { compactionController: ownCompactionController } : {},
792
+ env
793
+ }).catch((err) => {
794
+ const reason = err instanceof Error && err.message.length > 0 ? err.message : String(err ?? "");
795
+ settle("failed", reason.length > 0 ? `child engine exited: ${reason}` : "child engine process exited unexpectedly");
796
+ }).finally(servers.release).then(() => {
797
+ return;
798
+ }, () => {
799
+ return;
800
+ });
801
+ handedOver = true;
802
+ } finally {
803
+ if (!handedOver)
804
+ servers.release();
805
+ }
720
806
  channel.host.output.write({ type: "user", text: liveText });
721
807
  }, resolveParentRules = function() {
722
808
  const live = runCtx.getParentRules?.();
@@ -807,7 +893,8 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
807
893
  for (const [name, cfg] of Object.entries(spec))
808
894
  childScopedMcpServers[name] = cfg;
809
895
  }
810
- const hasChildScopedMcpServers = Object.keys(childScopedMcpServers).length > 0;
896
+ const ownServerNames = new Set(Object.entries(childScopedMcpServers).filter(([, cfg]) => cfg.type !== "sdk").map(([name]) => name));
897
+ const parentVisibleServers = () => new Set(parentMcp?.visibleServerNames?.() ?? []);
811
898
  const workspaceResult = await createWorkspace({
812
899
  parentCwd: inherit.sessionRoot,
813
900
  ...req.isolation !== undefined ? { isolation: req.isolation } : {},
@@ -852,6 +939,8 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
852
939
  let currentSink;
853
940
  let currentSettle;
854
941
  let currentUsage;
942
+ let lastGenerationDone = Promise.resolve();
943
+ let pendingResume;
855
944
  const baseConfig = {
856
945
  sessionId: runCtx.parentSessionId,
857
946
  cwd: workspace.root,
@@ -877,13 +966,15 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
877
966
  ...deps.parentSandbox !== undefined ? { sandbox: deps.parentSandbox } : {},
878
967
  ...req.definition?.maxTurns !== undefined ? { maxTurns: req.definition.maxTurns } : {},
879
968
  ...req.outputFormat !== undefined ? { outputFormat: req.outputFormat } : {},
880
- ...inherit.outputStyle !== undefined ? { outputStyle: inherit.outputStyle } : {},
881
- ...hasChildScopedMcpServers ? { mcpServers: childScopedMcpServers } : {}
969
+ ...inherit.outputStyle !== undefined ? { outputStyle: inherit.outputStyle } : {}
882
970
  };
883
971
  const initialMessages = buildForkInitialMessages(inherit, req.parentToolUseId);
884
972
  const resolvedSystemPrompt = [inherit.systemPrompt, req.fork === true ? undefined : req.definition?.prompt].filter((s) => s !== undefined && s.length > 0).join(`
885
973
 
886
974
  `);
975
+ let serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), undefined, parentMcp?.declaredServers);
976
+ pendingServerRelease = serverAllocation.release;
977
+ definitionWarnings.push(...serverAllocation.notes);
887
978
  const firstTurnText = req.fork === true ? buildForkDirectiveText({ prompt: req.prompt, ...req.isolation === "worktree" ? { worktree: { parentRoot: inherit.sessionRoot, worktreeRoot: workspace.root } } : {} }) : [req.definition?.initialPrompt, req.prompt, definitionWarnings.length > 0 ? `
888
979
  [winter: ${definitionWarnings.join("; ")}]` : undefined].filter((s) => s !== undefined && s.length > 0).join(`
889
980
 
@@ -987,7 +1078,18 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
987
1078
  writer?.writeMetadata({ ...record });
988
1079
  }
989
1080
  record.status = "running";
990
- startGeneration(generationConfig(resumeMode), rebuilt, msg.body, resolvedSystemPrompt);
1081
+ const previous = serverAllocation.actual;
1082
+ const ticket = { stopped: false };
1083
+ pendingResume = ticket;
1084
+ await Promise.race([lastGenerationDone, new Promise((resolve) => setTimeout(resolve, GENERATION_TEARDOWN_WAIT_MS).unref?.())]);
1085
+ pendingResume = undefined;
1086
+ if (ticket.stopped)
1087
+ return { status: "unavailable", messageId: msg.messageId, retryable: false, reason: `child ${agentId} was stopped while resuming` };
1088
+ serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), previous, parentMcp?.declaredServers);
1089
+ const moved = [...serverAllocation.actual].filter(([declared, name]) => previous.get(declared) !== name).map(([declared, name]) => `this agent's own "${declared}" is now connected as "${name}" (its tools are named mcp__${name}__<tool>)`);
1090
+ startGeneration(generationConfig(resumeMode), rebuilt, moved.length > 0 ? `${msg.body}
1091
+
1092
+ [winter: ${moved.join("; ")}]` : msg.body, resolvedSystemPrompt, serverAllocation);
991
1093
  return { status: "resumed_and_delivered", messageId: msg.messageId };
992
1094
  },
993
1095
  async result() {
@@ -996,6 +1098,12 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
996
1098
  async stop() {
997
1099
  if (record.status !== "running")
998
1100
  return;
1101
+ if (pendingResume !== undefined) {
1102
+ pendingResume.stopped = true;
1103
+ record.status = "stopped";
1104
+ writer?.writeMetadata({ ...record });
1105
+ return;
1106
+ }
999
1107
  currentSettle?.("stopped", "stopped by request");
1000
1108
  },
1001
1109
  usage() {
@@ -1005,12 +1113,14 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
1005
1113
  try {
1006
1114
  req.onSpawned?.(handle);
1007
1115
  } catch {}
1008
- startGeneration(generationConfig(inherit.policy.effectiveMode), initialMessages, firstTurnText, resolvedSystemPrompt);
1116
+ startGeneration(generationConfig(inherit.policy.effectiveMode), initialMessages, firstTurnText, resolvedSystemPrompt, serverAllocation);
1009
1117
  spawnRegistered = false;
1118
+ pendingServerRelease = undefined;
1010
1119
  return handle;
1011
1120
  } catch (err) {
1012
1121
  if (spawnRegistered)
1013
1122
  releaseSpawn(agentId);
1123
+ pendingServerRelease?.();
1014
1124
  throw err;
1015
1125
  }
1016
1126
  }
@@ -1305,7 +1415,8 @@ async function foldProviderStream(stream, sink) {
1305
1415
  ...event.cacheWriteTokens !== undefined ? { cacheWriteTokens: event.cacheWriteTokens } : {},
1306
1416
  ...event.cacheWrite1hTokens !== undefined ? { cacheWrite1hTokens: event.cacheWrite1hTokens } : {},
1307
1417
  ...event.cacheMiss !== undefined ? { cacheMiss: event.cacheMiss } : {},
1308
- ...event.thinkingBlocksDropped !== undefined ? { thinkingBlocksDropped: event.thinkingBlocksDropped } : {}
1418
+ ...event.thinkingBlocksDropped !== undefined ? { thinkingBlocksDropped: event.thinkingBlocksDropped } : {},
1419
+ ...event.reasoningTokens !== undefined ? { reasoningTokens: event.reasoningTokens } : {}
1309
1420
  };
1310
1421
  break;
1311
1422
  case "retry":
@@ -1902,7 +2013,8 @@ function buildSessionProvider(opts) {
1902
2013
  classifier = createModelClassifier2({
1903
2014
  provider: buildProvider(classifierResolved, classifierRouteRaw.authRef !== undefined ? { authRef: classifierRouteRaw.authRef } : {}),
1904
2015
  model: classifierResolved.providerModelId,
1905
- ...config.brand !== undefined ? { instructionsFile: config.brand.instructionsFile } : {}
2016
+ ...config.brand !== undefined ? { instructionsFile: config.brand.instructionsFile } : {},
2017
+ forcedToolChoiceUnsupported: classifierResolved.descriptor?.unsupportedParameters.includes("tool_choice.tool") === true
1906
2018
  });
1907
2019
  } catch (err) {
1908
2020
  classifier = undefined;
@@ -1915,7 +2027,8 @@ function buildSessionProvider(opts) {
1915
2027
  classifier = createModelClassifier2({
1916
2028
  provider: buildProvider(resolved),
1917
2029
  model: resolved.providerModelId,
1918
- ...config.brand !== undefined ? { instructionsFile: config.brand.instructionsFile } : {}
2030
+ ...config.brand !== undefined ? { instructionsFile: config.brand.instructionsFile } : {},
2031
+ forcedToolChoiceUnsupported: resolved.descriptor?.unsupportedParameters.includes("tool_choice.tool") === true
1919
2032
  });
1920
2033
  classifierIdentity = { modelKey: resolved.modelKey };
1921
2034
  }
@@ -3009,55 +3122,6 @@ function memoryDirFor(input) {
3009
3122
  return join6(input.storeHome ?? input.home, "projects", memoryProjectKeyFor(input.cwd, input.env), "memory");
3010
3123
  }
3011
3124
 
3012
- // src/context/plan-mode.ts
3013
- import { DEFAULT_PLANS_DIRECTORY } from "@yanlinglabs/winter-agent-sdk";
3014
- var PLAN_MODE_ENFORCEMENT = [
3015
- "This session is in PLAN MODE. Writes are withheld: file edits, file creation and state-changing shell commands are refused by the permission engine until the plan is approved. This is enforcement, not etiquette — attempting one wastes a turn and changes nothing.",
3016
- "Reading is unrestricted. Investigate as thoroughly as the plan needs: read files, search, run read-only commands, and inspect whatever settles an open question."
3017
- ].join(`
3018
-
3019
- `);
3020
- var DEFAULT_PLAN_BODY = [
3021
- "Produce a plan the user can approve or correct without having to guess what you intend.",
3022
- "Ground it in what you actually found. A plan that names the real files, the real functions and the real call sites is reviewable; one written from assumption is a proposal to find out later.",
3023
- "Say what will change and where, in the order it will happen, and name anything you deliberately are NOT changing when a reader might expect otherwise.",
3024
- "Call out the parts that are irreversible, that touch data, or that you are least sure about — those are the parts the user is being asked to approve.",
3025
- "Keep it proportionate. A one-file change needs a short paragraph, not a document; a migration needs the detail.",
3026
- "If investigation shows the request cannot work as stated, say so and propose the alternative instead of planning around the problem."
3027
- ].join(`
3028
-
3029
- `);
3030
- var PLAN_MODE_PROTOCOL = [
3031
- "When the plan is ready, present it and call the `ExitPlanMode` tool with it. Do not begin implementing first: leaving plan mode is the user's decision, and the tool call is how you ask for it.",
3032
- "If the user responds with corrections rather than approval, revise the plan and ask again — you are still in plan mode until they say otherwise."
3033
- ].join(`
3034
-
3035
- `);
3036
- var PLANS_DIRECTORY_MAX_CHARS = 200;
3037
- var PLANS_DIRECTORY_PATTERN = /^[A-Za-z0-9 ._~/-]+$/;
3038
- function renderablePlansDirectory(raw, fallback) {
3039
- if (raw.length === 0 || raw.length > PLANS_DIRECTORY_MAX_CHARS)
3040
- return fallback;
3041
- if (!PLANS_DIRECTORY_PATTERN.test(raw))
3042
- return fallback;
3043
- if (raw.trim().length === 0)
3044
- return fallback;
3045
- return raw;
3046
- }
3047
- function renderPlanModeBlock(input) {
3048
- const host = input.hostPlanBody?.trim();
3049
- const body = host === undefined || host.length === 0 ? DEFAULT_PLAN_BODY : host;
3050
- return [
3051
- "## Plan mode",
3052
- PLAN_MODE_ENFORCEMENT,
3053
- body,
3054
- PLAN_MODE_PROTOCOL,
3055
- `If the user asks for the plan as a file, write it under ${renderablePlansDirectory(input.plansDirectory, input.plansDirectoryFallback ?? DEFAULT_PLANS_DIRECTORY)}.`
3056
- ].join(`
3057
-
3058
- `);
3059
- }
3060
-
3061
3125
  // src/context/assembler.ts
3062
3126
  function isPresetOption(value) {
3063
3127
  return typeof value === "object" && value !== null && !Array.isArray(value) && value.type === "preset";
@@ -3145,11 +3209,6 @@ function createSystemPromptAssembler(deps = {}) {
3145
3209
  ...region.callerDynamicBlocks,
3146
3210
  styleBody,
3147
3211
  input.agentPrompt,
3148
- input.planMode ? renderPlanModeBlock({
3149
- plansDirectory: config.plansDirectory ?? settings?.plansDirectory ?? `${brand.projectDirName}/plans`,
3150
- plansDirectoryFallback: `${brand.projectDirName}/plans`,
3151
- ...input.hostPlanBody !== undefined ? { hostPlanBody: input.hostPlanBody } : {}
3152
- }) : undefined,
3153
3212
  region.excludeDynamicSections || memoryDir === undefined ? undefined : renderAutoMemorySection(memoryDir, brand.instructionsFile),
3154
3213
  region.excludeDynamicSections ? undefined : renderEnvironmentSection(environment)
3155
3214
  ];
@@ -3208,6 +3267,15 @@ function createSystemPromptAssembler(deps = {}) {
3208
3267
  entries.push(["auto memory", renderAutoMemoryContextValue(memoryDir, brand.instructionsFile)]);
3209
3268
  }
3210
3269
  return entries;
3270
+ },
3271
+ planModeInput(input) {
3272
+ const config = input.config;
3273
+ const { settings, brand } = resolveContext(deps, input);
3274
+ return {
3275
+ plansDirectory: config.plansDirectory ?? settings?.plansDirectory ?? `${brand.projectDirName}/plans`,
3276
+ plansDirectoryFallback: `${brand.projectDirName}/plans`,
3277
+ ...input.hostPlanBody !== undefined ? { hostPlanBody: input.hostPlanBody } : {}
3278
+ };
3211
3279
  }
3212
3280
  };
3213
3281
  }
@@ -5384,7 +5452,8 @@ function describeCatalogModel(catalog, model, providerId) {
5384
5452
  ...row.inlineToolDefinitions !== undefined ? { toolChanges: "inline" } : row.midConversationToolChanges !== undefined ? { toolChanges: "reference" } : {},
5385
5453
  ...row.clientToolSearch?.value === true ? { clientToolSearch: true } : {},
5386
5454
  ...row.additionalToolsItem?.value === true ? { additionalToolsItem: true } : {},
5387
- ...row.allowedToolsChoice?.value === true ? { allowedToolsChoice: true } : {}
5455
+ ...row.allowedToolsChoice?.value === true ? { allowedToolsChoice: true } : {},
5456
+ ...row.undeclaredToolCalls?.value === true ? { undeclaredToolCalls: true } : {}
5388
5457
  };
5389
5458
  const description = {
5390
5459
  ...row.displayName.length > 0 ? { displayName: row.displayName } : {},
package/dist/index.js CHANGED
@@ -53,7 +53,8 @@ import {
53
53
  DEFAULT_CLASSIFIER_TIMEOUT_MS2,
54
54
  createModelClassifier2,
55
55
  selectClassifierRoute2
56
- } from "./index-9qgkpv56.js";
56
+ } from "./index-mftq279y.js";
57
+ import"./index-8jgwp1px.js";
57
58
  import"./index-bef62z3r.js";
58
59
  import {
59
60
  createInMemoryChannel2
@@ -1,7 +1,22 @@
1
1
  import type { McpServerConfigForProcessTransport, McpVersionNegotiation } from "@yanlinglabs/winter-agent-sdk";
2
2
  import { type InProcessMcpServer } from "./transports/sdk.js";
3
3
  import { type ElicitationAsker } from "./elicitation.js";
4
- export type McpConnectErrorCode = "timeout" | "spawn_failed" | "handshake_failed" | "needs_auth" | "unknown";
4
+ /**
5
+ * WHY a connection attempt failed -- a category, not a message (the message is for humans; this is what
6
+ * the state board's `errorCode`, the web search backend and the `'auto'` legacy retry read).
7
+ *
8
+ * WS-24 split two causes out of `handshake_failed`, which a failed version negotiation used to land in
9
+ * whatever had gone wrong:
10
+ * - `version_mismatch`: the server and this client share no protocol version -- a `{pin}` the server
11
+ * did not offer, or a server that named the versions it speaks and none is ours. Retrying cannot
12
+ * help, so the `'auto'` legacy retry never spends a second connection on it.
13
+ * - `transport_closed`: the server ended the connection under the attempt -- a stdio server that
14
+ * exited by itself (on the `server/discover` probe, or during `initialize`). Observed on the
15
+ * transport (`WinterStdioTransport.exitedOnItsOwn`), never inferred from message text.
16
+ * A probe that went unanswered is `timeout` and a probe answered with an HTTP error keeps
17
+ * `handshake_failed` plus its `httpStatus`, as before.
18
+ */
19
+ export type McpConnectErrorCode = "timeout" | "spawn_failed" | "handshake_failed" | "version_mismatch" | "transport_closed" | "needs_auth" | "unknown";
5
20
  export declare class McpConnectError extends Error {
6
21
  readonly code: McpConnectErrorCode;
7
22
  /**
@@ -59,6 +59,8 @@ export declare function stdioFixtureCommand(): {
59
59
  export declare function pingFixtureCommand(opts?: {
60
60
  label?: string;
61
61
  delayMs?: number;
62
+ onProbe?: "answer" | "silent" | "exit";
63
+ spawnLog?: string;
62
64
  }): {
63
65
  command: string;
64
66
  args: string[];
@@ -20,11 +20,30 @@ export declare class WinterStdioTransport implements Transport {
20
20
  private child;
21
21
  private readonly readBuffer;
22
22
  private stderrTailBuffer;
23
+ private exitedOnItsOwnFlag;
23
24
  onclose?: () => void;
24
25
  onerror?: (error: Error) => void;
25
26
  onmessage?: (message: JSONRPCMessage) => void;
26
27
  constructor(opts: WinterStdioTransportOptions);
27
28
  get pid(): number | null;
29
+ /**
30
+ * WS-24: the running child's stderr stream (`null` before `start()` and after exit) -- the shape of
31
+ * the SDK's own `StdioClientTransport.stderr`. Declared for one reason: the v2 client decides how to
32
+ * read a `server/discover` probe by DUCK TYPING (`"stderr" in transport && "pid" in transport` ->
33
+ * "stdio"), and only as stdio does an unanswered probe settle the LEGACY era on the same pipe (a
34
+ * server that ignores unknown methods keeps its one process); read as "http", the silence is an outage
35
+ * and `connectMcpServer`'s legacy retry has to respawn the server. `client.test.ts` pins the single
36
+ * spawn, so a future client that stops recognising this transport fails there, not silently. Nothing
37
+ * reads from the stream through this getter: the tail below stays the one reader.
38
+ */
39
+ get stderr(): NodeJS.ReadableStream | null;
40
+ /**
41
+ * WS-24: the server exited BY ITSELF -- its process closed while this transport still held it, never
42
+ * through `close()` (which lets go of the child before killing it). This is how `connectMcpServer`
43
+ * tells a server that died under the attempt (`transport_closed`: e.g. one that exits on the
44
+ * `server/discover` probe) from one the client tore down after a failed attempt of its own.
45
+ */
46
+ get exitedOnItsOwn(): boolean;
28
47
  start(): Promise<void>;
29
48
  get stderrTail(): string;
30
49
  private withStderrTail;
@@ -0,0 +1,28 @@
1
+ import type { McpServerConfigForProcessTransport } from "@yanlinglabs/winter-agent-sdk";
2
+ import { type ConnectedMcpClient } from "./mcp/client.js";
3
+ import type { ElicitationAsker } from "./mcp/elicitation.js";
4
+ export { McpConnectError, resolveVersionNegotiation, type ConnectedMcpClient, type McpConnectErrorCode, type McpToolInfo, type McpToolAnnotationsInfo, type McpResourceInfo, type McpResourceContent, type McpToolCallResult, } from "./mcp/client.js";
5
+ export { createElicitationAsker, type ElicitationAction, type ElicitationAsker, type ElicitationRequestPayload, type ElicitationResultPayload, type ElicitationSender, } from "./mcp/elicitation.js";
6
+ /**
7
+ * What a HOST connects with -- deliberately narrower than the runtime's own options: no in-process
8
+ * (`type: "sdk"`) server (a host that has the server object in hand does not need a protocol client to
9
+ * reach it), no redirect refusal (the runtime's web search backend's concern) and no list-changed hook
10
+ * (the runtime's lifecycle owns re-registration). Everything it references is exported from here or from
11
+ * `@yanlinglabs/winter-agent-sdk`, so this subpath's declarations reach into nothing private.
12
+ */
13
+ export interface ConnectMcpServerOptions {
14
+ /** The server's name: prefixes its diagnostics and is `ConnectedMcpClient.serverName`. */
15
+ name: string;
16
+ /** A stdio, Streamable HTTP or SSE server config (with its optional `versionNegotiation`). */
17
+ config: Exclude<McpServerConfigForProcessTransport, {
18
+ type: "sdk";
19
+ }>;
20
+ /** The connection attempt's whole budget, in ms (never a tool call's timeout). */
21
+ connectTimeoutMs: number;
22
+ /** Answers a server's elicitation; `createElicitationAsker(undefined)` declines every one deterministically. */
23
+ elicitationAsk: ElicitationAsker;
24
+ /** REQUIRED for a stdio server: the directory it starts in (never the host process's cwd by default). */
25
+ cwd?: string;
26
+ }
27
+ /** Connect ONE MCP server and return its live client. Rejects with a typed `McpConnectError` (see its `code`). */
28
+ export declare function connectMcpServer(opts: ConnectMcpServerOptions): Promise<ConnectedMcpClient>;
@@ -0,0 +1,23 @@
1
+ import {
2
+ createElicitationAsker2,
3
+ McpConnectError2,
4
+ resolveVersionNegotiation2,
5
+ connectMcpServer2
6
+ } from "./index-8jgwp1px.js";
7
+
8
+ // src/mcp-client.ts
9
+ function connectMcpServer(opts) {
10
+ return connectMcpServer2({
11
+ name: opts.name,
12
+ config: opts.config,
13
+ connectTimeoutMs: opts.connectTimeoutMs,
14
+ elicitationAsk: opts.elicitationAsk,
15
+ ...opts.cwd !== undefined ? { cwd: opts.cwd } : {}
16
+ });
17
+ }
18
+ export {
19
+ McpConnectError2 as McpConnectError,
20
+ connectMcpServer,
21
+ createElicitationAsker2 as createElicitationAsker,
22
+ resolveVersionNegotiation2 as resolveVersionNegotiation
23
+ };
@@ -18,6 +18,11 @@ export interface SymlinkBothEndsResult {
18
18
  }
19
19
  export declare function resolveRealTarget(path: string): string;
20
20
  export declare function resolveSymlinkTargetChain(path: string): string | undefined;
21
+ export interface SymlinkEnds {
22
+ target: string;
23
+ chainTarget: string | undefined;
24
+ }
25
+ export declare function resolveSymlinkEnds(path: string): SymlinkEnds;
21
26
  export declare function checkSymlinkBothEnds(path: string, matcher: (candidatePath: string) => boolean): SymlinkBothEndsResult;
22
27
  export declare function matchFileRuleAtBothEnds(pattern: string, opts: MatchFileRuleOptions): boolean;
23
28
  export interface FileRuleEntry {
@@ -0,0 +1,22 @@
1
+ /** Which spawn site a group came from -- for diagnostics only; the host treats every group alike. */
2
+ export type ProcessGroupKind = "command" | "mcp_stdio" | "hook" | "workflow";
3
+ export interface ProcessGroupChange {
4
+ op: "add" | "remove";
5
+ pgid: number;
6
+ kind: ProcessGroupKind;
7
+ }
8
+ /**
9
+ * Record `pgid` (the pid of a child spawned `detached: true`, i.e. its group leader) as live. Returns
10
+ * the release, which is idempotent -- a spawn site may reach its "the leader is gone" point by more
11
+ * than one path (`close` after `error`, an explicit `close()` racing the exit event).
12
+ */
13
+ export declare function trackProcessGroup(pgid: number, kind: ProcessGroupKind): () => void;
14
+ /** Every group currently recorded as live, oldest first. */
15
+ export declare function liveProcessGroups(): ReadonlyArray<{
16
+ pgid: number;
17
+ kind: ProcessGroupKind;
18
+ }>;
19
+ /** Observe every add/remove from now on. Returns the unsubscribe. */
20
+ export declare function onProcessGroupChange(listener: (change: ProcessGroupChange) => void): () => void;
21
+ /** Test seam: forget everything (a test that spawns and kills in one realm must not see another test's groups). */
22
+ export declare function resetProcessGroupsForTest(): void;
@@ -23,6 +23,18 @@ export interface ModelClassifierOptions {
23
23
  maxContextChars?: number;
24
24
  /** P7a fix wave (item 5, M-1): the running brand's instructions file, so the classifier prompt labels the operator's own file correctly. */
25
25
  instructionsFile?: string;
26
+ /**
27
+ * WS-24 (M-1 fix round): whether this row's descriptor lists `tool_choice.tool` in its
28
+ * `unsupportedParameters` -- the SAME evidence `resolveToolChoice` (`adapters/anthropic/
29
+ * messages.ts`) gates its forced-choice-to-`auto` downgrade on (today: the anthropic/console Opus
30
+ * 5.5 and Fable 5.1 rows). ONLY on such a row does a text reply get the text-fallback path
31
+ * (`collapseTextFallback`): the request's own forced `tool_choice` is a documented lie for it, so a
32
+ * text reply is the expected shape of an honest answer, not a misbehaving model. Everywhere else, a
33
+ * text reply IS a misbehaving model (a real forced choice reached it and it ignored the tool
34
+ * anyway) and stays `no_tool_call` -- never given a chance to recover, which would otherwise let
35
+ * ANY model's ordinary refusal-to-call-the-tool quietly become a verdict. Defaults to `false`.
36
+ */
37
+ forcedToolChoiceUnsupported?: boolean;
26
38
  /**
27
39
  * Test seam: observes what was actually sent and what came back, WITHOUT the classifier logging
28
40
  * anything itself. Nothing in this file writes to a log — a permission review's request contains
@@ -478,7 +478,7 @@ export declare class TranscriptWriter implements SessionPersistence {
478
478
  private dialectRecord;
479
479
  static readBack(store: SessionStore, key: SessionKey): Promise<SessionStoreEntry[]>;
480
480
  }
481
- export declare const RUNTIME_ENGINE_VERSION = "0.0.27";
481
+ export declare const RUNTIME_ENGINE_VERSION = "0.0.28";
482
482
  export interface ResolvedEngineSession {
483
483
  config: RuntimeConfig;
484
484
  store: SessionPersistence | undefined;
@@ -21,7 +21,20 @@ export declare const PROVIDER_STATE_SUBPATH: "provider-state";
21
21
  * they cannot drift from what the deletion transaction actually removes.
22
22
  */
23
23
  export { PROVIDER_STATE_FILE_SUFFIX };
24
- export type ProviderStateKind = "origin" | "native-state" | "summary" | "handoff" | "reasoning-blocks" | "effort" | "tool-epoch" | "tool-changes" | "decoration";
24
+ export type ProviderStateKind = "origin" | "native-state" | "summary" | "handoff" | "reasoning-blocks" | "effort" | "tool-epoch" | "tool-changes" | "decoration" | "feature-rejected";
25
+ /**
26
+ * WS-24: the request features an API can refuse once and the session then stops sending, on that
27
+ * provider+model: the per-message effort beta, a mid-conversation tool change, OpenAI's client
28
+ * `tool_search`, and `tool_choice: allowed_tools`.
29
+ */
30
+ export declare const REJECTABLE_FEATURES: readonly ["per-message-effort", "tool-changes", "client-tool-search", "allowed-tools"];
31
+ export type RejectableFeature = (typeof REJECTABLE_FEATURES)[number];
32
+ /** WS-24: every `feature-rejected` record, as `{provider, model, feature}` -- anchor presence is deliberately not consulted (see the kind's note above). */
33
+ export declare function rejectedFeaturesFrom(records: readonly ProviderStateRecord[]): Array<{
34
+ provider: string;
35
+ model: string;
36
+ feature: RejectableFeature;
37
+ }>;
25
38
  /**
26
39
  * One sidecar record. The envelope (`type`/`uuid`/`timestamp`) plus R6-7's own payload fields.
27
40
  *