pilotswarm-sdk 0.5.37 → 0.5.39

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.
Files changed (52) hide show
  1. package/api/src/http-api-transport.js +26 -7
  2. package/api/src/protocol.js +16 -7
  3. package/dist/agent-manager-tools.d.ts.map +1 -1
  4. package/dist/agent-manager-tools.js +12 -2
  5. package/dist/agent-manager-tools.js.map +1 -1
  6. package/dist/agent-package-installer.d.ts +2 -0
  7. package/dist/agent-package-installer.d.ts.map +1 -1
  8. package/dist/agent-package-installer.js +1 -0
  9. package/dist/agent-package-installer.js.map +1 -1
  10. package/dist/agent-package-service.d.ts +1 -1
  11. package/dist/agent-package-service.d.ts.map +1 -1
  12. package/dist/agent-package-service.js +79 -11
  13. package/dist/agent-package-service.js.map +1 -1
  14. package/dist/cms-migrations.d.ts.map +1 -1
  15. package/dist/cms-migrations.js +117 -0
  16. package/dist/cms-migrations.js.map +1 -1
  17. package/dist/cms.d.ts +50 -0
  18. package/dist/cms.d.ts.map +1 -1
  19. package/dist/cms.js +65 -0
  20. package/dist/cms.js.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/managed-session.d.ts +4 -7
  26. package/dist/managed-session.d.ts.map +1 -1
  27. package/dist/managed-session.js +99 -18
  28. package/dist/managed-session.js.map +1 -1
  29. package/dist/management-client.d.ts.map +1 -1
  30. package/dist/management-client.js +8 -1
  31. package/dist/management-client.js.map +1 -1
  32. package/dist/model-providers.d.ts +142 -6
  33. package/dist/model-providers.d.ts.map +1 -1
  34. package/dist/model-providers.js +117 -1
  35. package/dist/model-providers.js.map +1 -1
  36. package/dist/session-manager.d.ts +97 -7
  37. package/dist/session-manager.d.ts.map +1 -1
  38. package/dist/session-manager.js +292 -31
  39. package/dist/session-manager.js.map +1 -1
  40. package/dist/session-proxy.d.ts +43 -0
  41. package/dist/session-proxy.d.ts.map +1 -1
  42. package/dist/session-proxy.js +296 -184
  43. package/dist/session-proxy.js.map +1 -1
  44. package/dist/web/generated-op-methods.d.ts +34 -5
  45. package/dist/web/generated-op-methods.d.ts.map +1 -1
  46. package/dist/web/generated-op-methods.js +2 -0
  47. package/dist/web/generated-op-methods.js.map +1 -1
  48. package/dist/worker.d.ts +23 -0
  49. package/dist/worker.d.ts.map +1 -1
  50. package/dist/worker.js +189 -17
  51. package/dist/worker.js.map +1 -1
  52. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  import { CopilotClient } from "@github/copilot-sdk";
2
2
  import { ManagedSession } from "./managed-session.js";
3
3
  import { SESSION_STATE_MISSING_PREFIX } from "./types.js";
4
+ import { applyReasoningEffortToProviderConfig } from "./model-providers.js";
4
5
  import { createFactTools } from "./facts-tools.js";
5
6
  import { createGraphTools } from "./graph-tools.js";
6
7
  import { createInspectTools, NO_VIEWER } from "./inspect-tools.js";
@@ -69,7 +70,51 @@ function isMissingDehydrateSnapshotError(error) {
69
70
  const message = error instanceof Error ? error.message : String(error ?? "");
70
71
  return /Session state directory not ready during dehydrate/i.test(message);
71
72
  }
72
- function buildEffectivePromptLayers(workerDefaults, config) {
73
+ /** Key under which a package copy's per-agent config (MCP) is registered. */
74
+ export function packageAgentKey(packageId, agentName) {
75
+ return `pkg\u0001${packageId}\u0001${agentName}`;
76
+ }
77
+ /** Owner key used to match a session owner against a package copy's owner. */
78
+ export function agentOwnerKey(owner) {
79
+ return owner?.provider && owner?.subject ? `${owner.provider}\u0001${owner.subject}` : null;
80
+ }
81
+ /**
82
+ * Which copy of an agent name does THIS session get?
83
+ *
84
+ * Runs per turn, so enabling/disabling a copy re-resolves exactly like a
85
+ * republish does — live sessions follow the registry's current answer, they
86
+ * are not pinned to a copy.
87
+ *
88
+ * FAIL CLOSED, matching resolveAgentDefinitionForCaller: a user-scope copy is
89
+ * PRIVATE to its owner, and the worker holds every tenant's copies at once.
90
+ * The order is (1) the session owner's OWN user copy, (2) the best copy that
91
+ * is public by construction — a deployment agent or a shared package. A
92
+ * user-scope copy owned by someone ELSE is NEVER served, even when it is the
93
+ * only copy of the name left: returning it would leak that owner's prompt,
94
+ * MCP grants, and tool handlers into this session (they all follow the copy
95
+ * picked here). undefined means "no package overlay for this session" — the
96
+ * same outcome as the agent having been deleted, which is correct.
97
+ */
98
+ export function pickAgentCopyForOwner(entry, ownerKey) {
99
+ if (!entry)
100
+ return undefined;
101
+ const copies = entry.copies?.length ? entry.copies : [entry];
102
+ if (ownerKey) {
103
+ const own = copies.find((copy) => copy.packageScope === "user" && agentOwnerKey(copy.packageOwner) === ownerKey);
104
+ if (own)
105
+ return own;
106
+ }
107
+ // The best PUBLIC copy, BY RANK not insertion order: a deployment agent
108
+ // (no packageScope) outranks a shared package, matching the worker's
109
+ // _finalizeAgentPromptLookup default. copies[] preserves load order, so
110
+ // picking by find() alone could hand back a shared package while a
111
+ // deployment agent of the same name exists — the two would disagree about
112
+ // which copy is the default. Never a foreign private copy, at any rank.
113
+ return copies.find((copy) => copy.packageScope == null)
114
+ ?? copies.find((copy) => copy.packageScope === "shared")
115
+ ?? undefined;
116
+ }
117
+ function buildEffectivePromptLayers(workerDefaults, config, sessionOwnerKey = null) {
73
118
  const boundAgentName = config.boundAgentName;
74
119
  const layerKind = config.promptLayering?.kind ?? (boundAgentName ? "app-agent" : undefined);
75
120
  const isPilotSwarmSystemAgent = layerKind === "pilotswarm-system-agent";
@@ -79,9 +124,9 @@ function buildEffectivePromptLayers(workerDefaults, config) {
79
124
  if (!isPilotSwarmSystemAgent && workerDefaults.appDefaultDescriptor)
80
125
  layers.push(workerDefaults.appDefaultDescriptor);
81
126
  if (boundAgentName) {
82
- const agentDescriptor = workerDefaults.agentPromptLookup?.[boundAgentName]?.descriptor;
83
- if (agentDescriptor)
84
- layers.push(agentDescriptor);
127
+ const copy = pickAgentCopyForOwner(workerDefaults.agentPromptLookup?.[boundAgentName], sessionOwnerKey);
128
+ if (copy?.descriptor)
129
+ layers.push(copy.descriptor);
85
130
  }
86
131
  return layers;
87
132
  }
@@ -96,6 +141,78 @@ function buildEffectivePromptLayers(workerDefaults, config) {
96
141
  *
97
142
  * @internal
98
143
  */
144
+ /**
145
+ * Capability declaration for a model the Copilot catalog does not know.
146
+ *
147
+ * Returns undefined when the catalog says nothing useful — an empty override
148
+ * is worse than none, since it would assert "no vision, no reasoning" rather
149
+ * than "unknown".
150
+ *
151
+ * @internal Exported for test/unit/byok-context-window.test.mjs only.
152
+ */
153
+ export function buildByokModelCapabilities(descriptor, contextTier) {
154
+ if (!descriptor)
155
+ return undefined;
156
+ const supports = {};
157
+ if (Array.isArray(descriptor.supportedReasoningEfforts) && descriptor.supportedReasoningEfforts.length > 0) {
158
+ supports.reasoningEffort = true;
159
+ }
160
+ if (descriptor.vision)
161
+ supports.vision = true;
162
+ const limits = {};
163
+ const sizes = descriptor.contextWindowSizes;
164
+ if (sizes && typeof sizes === "object") {
165
+ // Prefer the session's tier, else the largest declared — the catalog
166
+ // is stating what the model can do, not what this turn will use.
167
+ const tierValue = contextTier ? sizes[contextTier] : undefined;
168
+ const window = Number.isFinite(tierValue)
169
+ ? Number(tierValue)
170
+ : Math.max(...Object.values(sizes).map((v) => Number(v)).filter((v) => Number.isFinite(v)), 0);
171
+ if (window > 0) {
172
+ limits.max_context_window_tokens = window;
173
+ // max_prompt_tokens is the one that actually does anything.
174
+ // Measured against @github/copilot-sdk 1.0.9 with kimi-k3, three
175
+ // arms, one message each, reading session.usage_info.tokenLimit:
176
+ // max_context_window_tokens alone -> 128000 (the runtime default)
177
+ // max_prompt_tokens alone -> 1048576
178
+ // both -> 1048576
179
+ // tokenLimit is what the child process divides by to decide when to
180
+ // compact, so without this line the declaration changes nothing and
181
+ // a 1M model still compacts at ~102K.
182
+ limits.max_prompt_tokens = window;
183
+ }
184
+ }
185
+ if (descriptor.vision && typeof descriptor.vision === "object") {
186
+ const v = {};
187
+ if (Number.isFinite(descriptor.vision.maxImages))
188
+ v.max_prompt_images = Number(descriptor.vision.maxImages);
189
+ if (Number.isFinite(descriptor.vision.maxImageBytes))
190
+ v.max_prompt_image_size = Number(descriptor.vision.maxImageBytes);
191
+ if (Array.isArray(descriptor.vision.supportedMediaTypes))
192
+ v.supported_media_types = descriptor.vision.supportedMediaTypes;
193
+ if (Object.keys(v).length > 0)
194
+ limits.vision = v;
195
+ }
196
+ // `limits` is the only part that does real work, so a supports-only block
197
+ // is dropped entirely.
198
+ //
199
+ // Declaring supports.reasoningEffort does NOT make the runtime send
200
+ // reasoning_effort for a BYOK provider — measured five ways, see the note
201
+ // at the sessionConfig call site. So a block carrying only `supports`
202
+ // buys nothing, while still overriding runtime state for every BYOK model
203
+ // in every deployment that has no contextWindowSizes in its catalog.
204
+ // Measured against waldemort's catalog: 8 of its 14 models declare
205
+ // supportedReasoningEfforts and no contextWindowSizes, so without this
206
+ // line they would each start receiving {supports:{reasoningEffort:true}}
207
+ // for no gain. A model that really can see gets limits.vision, so vision
208
+ // declarations still survive.
209
+ if (Object.keys(limits).length === 0)
210
+ return undefined;
211
+ return {
212
+ ...(Object.keys(supports).length > 0 ? { supports } : {}),
213
+ limits,
214
+ };
215
+ }
99
216
  export class SessionManager {
100
217
  githubToken;
101
218
  /**
@@ -157,6 +274,10 @@ export class SessionManager {
157
274
  sessionConfigs = new Map();
158
275
  /** Worker-level tool registry — shared reference from PilotSwarmWorker. */
159
276
  toolRegistry = new Map();
277
+ /** Per-package tool maps, so a session prefers ITS package's handler on a name collision. */
278
+ packageToolRegistry = null;
279
+ /** Names registered by deployment code — a package tool must never shadow these. */
280
+ staticToolNames = null;
160
281
  /** Worker-level defaults for building blocks. */
161
282
  workerDefaults;
162
283
  /** Base directory for local session state files. */
@@ -264,9 +385,11 @@ export class SessionManager {
264
385
  */
265
386
  async getModelVisionInfo(modelRef, opts) {
266
387
  let sdkModelName = String(modelRef || "").trim();
388
+ // Hoisted out of the try: the declared-capability check below needs it.
389
+ let descriptor;
267
390
  try {
268
391
  const normalized = this.normalizeModelRef(modelRef || undefined);
269
- const descriptor = this.workerDefaults.modelProviders?.getDescriptor(normalized);
392
+ descriptor = this.workerDefaults.modelProviders?.getDescriptor(normalized);
270
393
  if (descriptor?.modelName)
271
394
  sdkModelName = descriptor.modelName;
272
395
  else if (normalized)
@@ -279,6 +402,28 @@ export class SessionManager {
279
402
  }
280
403
  if (!sdkModelName)
281
404
  return { modelId: "", known: false, vision: false };
405
+ // A DECLARED capability wins, and skips the catalog entirely.
406
+ //
407
+ // The catalog below is the Copilot model list. A BYOK model — Fireworks,
408
+ // a local vLLM — is never in it, so the lookup returns no entry, this
409
+ // function reports `vision: false`, and the attachment gate drops every
410
+ // image before its bytes are fetched. No amount of catalog querying can
411
+ // fix that; only the operator knows, and this is where they say so.
412
+ //
413
+ // Absent a declaration nothing changes: Copilot and Azure models keep
414
+ // resolving from the catalog exactly as before, and an unknown model
415
+ // still fails closed.
416
+ if (descriptor?.vision) {
417
+ const declared = descriptor.vision;
418
+ return {
419
+ modelId: sdkModelName,
420
+ known: true,
421
+ vision: true,
422
+ ...(Array.isArray(declared.supportedMediaTypes) ? { supportedMediaTypes: declared.supportedMediaTypes } : {}),
423
+ ...(Number.isFinite(declared.maxImages) ? { maxImages: Number(declared.maxImages) } : {}),
424
+ ...(Number.isFinite(declared.maxImageBytes) ? { maxImageBytes: Number(declared.maxImageBytes) } : {}),
425
+ };
426
+ }
282
427
  // Consult the catalog on the client that will serve this session's
283
428
  // turns. Prefer the recorded binding; on a cold worker the gate runs
284
429
  // before getOrCreate records one, so fall through to the same
@@ -340,9 +485,18 @@ export class SessionManager {
340
485
  ...(Number.isFinite(visionLimits?.max_prompt_image_size) ? { maxImageBytes: Number(visionLimits.max_prompt_image_size) } : {}),
341
486
  };
342
487
  }
343
- /** Set the worker-level tool registry. Called by PilotSwarmWorker. */
344
- setToolRegistry(registry) {
488
+ /**
489
+ * Set the worker-level tool registry. Called by PilotSwarmWorker.
490
+ *
491
+ * `opts.byPackage` carries each agent package's own tool map so a session
492
+ * bound to a package copy resolves colliding tool names to ITS copy's
493
+ * handler. `opts.staticNames` marks deployment-registered tools, which win
494
+ * every collision (a package must not shadow deployment code).
495
+ */
496
+ setToolRegistry(registry, opts) {
345
497
  this.toolRegistry = registry;
498
+ this.packageToolRegistry = opts?.byPackage ?? null;
499
+ this.staticToolNames = opts?.staticNames ?? null;
346
500
  }
347
501
  /** Set the cluster facts store for always-on facts tools. */
348
502
  setFactStore(factStore) {
@@ -848,9 +1002,25 @@ export class SessionManager {
848
1002
  const effectiveSerializableConfig = inheritedToolNames.length > 0
849
1003
  ? { ...serializableConfig, toolNames: inheritedToolNames }
850
1004
  : serializableConfig;
1005
+ // Which copy of the bound agent does this session get? Owner-aware:
1006
+ // the session owner's own user-scope package copy shadows the shared
1007
+ // one, the same rule the registry resolver applies at create time.
1008
+ // Resolved once here; prompt, descriptor, MCP, and tool-handler picks
1009
+ // below all follow the same copy so a session can never mix copies.
1010
+ const boundAgentEntry = effectiveSerializableConfig.boundAgentName
1011
+ ? this.workerDefaults.agentPromptLookup?.[effectiveSerializableConfig.boundAgentName]
1012
+ : undefined;
1013
+ // A PACKAGE agent (as opposed to a deployment/inline agent) carries a
1014
+ // packageId on its entry or on any of its copies. This is load-bearing
1015
+ // for MCP resolution: a package agent must never fall back to the
1016
+ // bare-name MCP key, which another copy of a shadowed name also wrote.
1017
+ const sessionOwnerKey = effectiveSerializableConfig.boundAgentName
1018
+ ? await this._sessionAgentOwnerKey(sessionId)
1019
+ : null;
1020
+ const boundAgentCopy = pickAgentCopyForOwner(boundAgentEntry, sessionOwnerKey);
851
1021
  // Resolve tools: merge per-session (setConfig) + registry (toolNames)
852
1022
  const storedConfig = this.sessionConfigs.get(sessionId);
853
- const resolvedTools = this._resolveTools(storedConfig, effectiveSerializableConfig);
1023
+ const resolvedTools = this._resolveTools(storedConfig, effectiveSerializableConfig, boundAgentCopy?.packageId);
854
1024
  const config = {
855
1025
  ...storedConfig,
856
1026
  ...effectiveSerializableConfig,
@@ -900,16 +1070,32 @@ export class SessionManager {
900
1070
  const registry = this.workerDefaults.modelProviders;
901
1071
  const effectiveModel = this.normalizeModelRef(config.model) || "";
902
1072
  const resolvedProvider = registry?.resolve(effectiveModel);
903
- const resolvedProviderConfig = this._resolveProviderConfig(effectiveModel);
904
- let sdkModelName = effectiveModel;
905
- let modelDescriptor;
906
- if (registry && effectiveModel) {
907
- const desc = registry.getDescriptor(effectiveModel);
908
- if (desc) {
909
- sdkModelName = desc.modelName;
910
- modelDescriptor = desc;
1073
+ const baseProviderConfig = this._resolveProviderConfig(effectiveModel);
1074
+ const sdkModelName = effectiveModel && registry?.getDescriptor(effectiveModel)?.modelName
1075
+ ? registry.getDescriptor(effectiveModel).modelName
1076
+ : effectiveModel;
1077
+ const modelDescriptor = registry && effectiveModel
1078
+ ? registry.getDescriptor(effectiveModel)
1079
+ : undefined;
1080
+ // `config.reasoningEffort` is passed to the SDK on its own below, and
1081
+ // for provider `type: github` that is what works. For a provider
1082
+ // declared `type: openai-proxy` the `@github/copilot` child process
1083
+ // drops it before the HTTP request is built, so it rides in the
1084
+ // provider baseUrl as a path prefix and the proxy at that baseUrl
1085
+ // strips it back off. Every other provider type is untouched, and a
1086
+ // session with no effort set is untouched.
1087
+ //
1088
+ // It used to ride in the model NAME. @github/copilot 1.0.79 parses
1089
+ // `model:key=value` as its own model-options syntax and rejects
1090
+ // unknown keys, so every such turn failed with
1091
+ // "Unknown model option key: effort". See
1092
+ // applyReasoningEffortToProviderConfig for the measurements.
1093
+ const resolvedProviderConfig = baseProviderConfig.provider
1094
+ ? {
1095
+ ...baseProviderConfig,
1096
+ provider: applyReasoningEffortToProviderConfig(baseProviderConfig.provider, modelDescriptor, config.reasoningEffort),
911
1097
  }
912
- }
1098
+ : baseProviderConfig;
913
1099
  // Context-window tier: only models whose catalog entry declares
914
1100
  // supportedContextTiers get the field at all. An explicit valid tier
915
1101
  // wins; otherwise fall back to the catalog default ("default" — the
@@ -921,6 +1107,12 @@ export class SessionManager {
921
1107
  ? config.contextTier
922
1108
  : (modelDescriptor?.defaultContextTier ?? "default"))
923
1109
  : undefined;
1110
+ // Built from the deployment's own catalog — the only place that knows
1111
+ // anything about a BYOK model. Deliberately AFTER the tier resolution
1112
+ // above: the declared window must match the tier this session actually
1113
+ // runs at, and before normalization config.contextTier can still be
1114
+ // unset or stale. See the modelCapabilities note at the config object.
1115
+ const byokModelCapabilities = buildByokModelCapabilities(modelDescriptor, config.contextTier);
924
1116
  // Resolve the per-user GitHub Copilot token only when a catalog
925
1117
  // is wired in. Skipping the await on the no-catalog path matters
926
1118
  // for the SessionManager unit tests that exercise lock ordering
@@ -963,7 +1155,7 @@ export class SessionManager {
963
1155
  // whole purpose is to change agents — so the strip applies only to the
964
1156
  // legacy id, and dies with it.
965
1157
  const isTunerSession = effectiveSerializableConfig.agentIdentity === "agent-tuner";
966
- const mutatingSystemToolNames = new Set(["send_session_message", "reply_session_message", "draw_canvas",
1158
+ const mutatingSystemToolNames = new Set(["send_session_message", "reply_session_message", "draw_canvas", "show_canvas",
967
1159
  "update_canvas"]);
968
1160
  const userTools = config.tools ?? [];
969
1161
  // Canvas tools are ROOT-only, and THIS is the declaration half of that
@@ -975,11 +1167,10 @@ export class SessionManager {
975
1167
  // dropped by the CLI (no handler, no response, turn hangs), which is
976
1168
  // strictly worse than an error. Fail open on an unreadable row: the
977
1169
  // per-turn handler still refuses with a clear message.
978
- const canvasToolNames = new Set(["draw_canvas", "update_canvas", "read_canvas"]);
979
- const isChildSession = Boolean(catalogRow?.parentSessionId);
1170
+ // Canvas tools are declared for EVERY session now — sub-agents draw
1171
+ // their own canvases (slots 1-5), independent of the parent's.
980
1172
  const systemTools = ManagedSession.systemToolDefs()
981
- .filter((tool) => !isTunerSession || !mutatingSystemToolNames.has(tool.name))
982
- .filter((tool) => !isChildSession || !canvasToolNames.has(tool.name));
1173
+ .filter((tool) => !isTunerSession || !mutatingSystemToolNames.has(tool.name));
983
1174
  const readOnlyTunerSubAgentToolNames = new Set(["check_agents", "list_sessions"]);
984
1175
  const subAgentTools = ManagedSession.subAgentToolDefs()
985
1176
  .filter((tool) => !isTunerSession || readOnlyTunerSubAgentToolNames.has(tool.name));
@@ -1108,15 +1299,31 @@ export class SessionManager {
1108
1299
  ];
1109
1300
  config.tools = persistentSessionTools;
1110
1301
  // Build system message: worker base + client override
1111
- const systemMessage = this._buildSystemMessage(sessionId, config);
1302
+ const systemMessage = this._buildSystemMessage(sessionId, config, sessionOwnerKey);
1112
1303
  // Per-agent MCP (capability-profiles Phase 1): a session gets the
1113
1304
  // base map (base-agent opt-ins + direct worker-config servers) plus
1114
1305
  // its bound agent's resolved server map — resolved worker-side at the
1115
1306
  // same chokepoint as the agent prompt. The deployment catalog is
1116
1307
  // never applied wholesale.
1117
- const boundAgentMcpServers = effectiveSerializableConfig.boundAgentName
1118
- ? this.workerDefaults.agentMcpServers?.[effectiveSerializableConfig.boundAgentName]
1119
- : undefined;
1308
+ //
1309
+ // Read the MCP map of the copy THIS session actually resolved to.
1310
+ // The worker registers a package agent's MCP under a package-qualified
1311
+ // key only, and reserves the bare name for deployment/inline agents —
1312
+ // so the resolved copy's own packageId is the discriminator:
1313
+ // • package copy resolved → its qualified key (never the bare name,
1314
+ // which another copy of a shadowed name could have written);
1315
+ // • deployment/inline copy → the bare name (its own MCP);
1316
+ // • no copy resolved (a foreign-private-only name) → no grants.
1317
+ // Keying off `boundAgentCopy.packageId` rather than "is any copy a
1318
+ // package" is load-bearing: when a deployment agent and a package
1319
+ // share a name and this session resolved the DEPLOYMENT copy, it must
1320
+ // still get the deployment agent's bare-key MCP, not an empty
1321
+ // qualified lookup.
1322
+ const boundAgentMcpServers = !effectiveSerializableConfig.boundAgentName || !boundAgentCopy
1323
+ ? undefined
1324
+ : boundAgentCopy.packageId
1325
+ ? this.workerDefaults.agentMcpServers?.[packageAgentKey(boundAgentCopy.packageId, effectiveSerializableConfig.boundAgentName)]
1326
+ : this.workerDefaults.agentMcpServers?.[effectiveSerializableConfig.boundAgentName];
1120
1327
  const effectiveMcpServers = {
1121
1328
  ...(this.workerDefaults.baseMcpServers ?? {}),
1122
1329
  ...(boundAgentMcpServers ?? {}),
@@ -1130,6 +1337,28 @@ export class SessionManager {
1130
1337
  // tool-pinning.ts for the why and the phase-2 opt-out path.
1131
1338
  tools: pinToolsNeverDefer(allTools),
1132
1339
  model: sdkModelName,
1340
+ // Tell the runtime what a BYOK model can do.
1341
+ //
1342
+ // A model outside the Copilot catalog has no capabilities, so the
1343
+ // runtime falls back to defaults — including a 128000 token limit
1344
+ // that makes a 1M-window model compact roughly 8x too early.
1345
+ // Declaring limits.max_prompt_tokens here fixes that; the reported
1346
+ // tokenLimit becomes the real window. Measured, see
1347
+ // buildByokModelCapabilities.
1348
+ //
1349
+ // It does NOT fix reasoning effort. Declaring
1350
+ // supports.reasoningEffort = true still does not put
1351
+ // `reasoning_effort` on the outgoing request for a BYOK provider —
1352
+ // verified on the multi-provider `providers`/`models` surface, with
1353
+ // enableConfigDiscovery on, and via the runtime's own
1354
+ // `model:defaultReasoningEffort=high` option. That is why
1355
+ // applyReasoningEffortToProviderConfig still exists.
1356
+ //
1357
+ // Only for non-github providers: Copilot models have real catalog
1358
+ // data and must not be overridden by our guesses.
1359
+ ...(resolvedProviderConfig.provider && byokModelCapabilities
1360
+ ? { modelCapabilities: byokModelCapabilities }
1361
+ : {}),
1133
1362
  ...(config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}),
1134
1363
  ...(config.contextTier ? { contextTier: config.contextTier } : {}),
1135
1364
  systemMessage: systemMessage
@@ -1593,12 +1822,23 @@ export class SessionManager {
1593
1822
  /**
1594
1823
  * Resolve tools from per-session config + worker-level registry.
1595
1824
  * Per-session tools take precedence over registry tools with the same name.
1825
+ *
1826
+ * `preferredPackageId` is the package copy this session's bound agent
1827
+ * resolved to. On a tool-name collision between two enabled packages
1828
+ * (scope shadowing publishes the same tool name twice), the session gets
1829
+ * ITS copy's handler instead of whichever package loaded last. Deployment
1830
+ * (static) tools still win every collision.
1596
1831
  */
1597
- _resolveTools(storedConfig, serializableConfig) {
1832
+ _resolveTools(storedConfig, serializableConfig, preferredPackageId) {
1598
1833
  const registryTools = [];
1834
+ const packageTools = preferredPackageId
1835
+ ? this.packageToolRegistry?.get(preferredPackageId)
1836
+ : undefined;
1599
1837
  if (serializableConfig.toolNames?.length) {
1600
1838
  for (const name of serializableConfig.toolNames) {
1601
- const tool = this.toolRegistry.get(name);
1839
+ const tool = (this.staticToolNames?.has(name) ? this.toolRegistry.get(name) : undefined)
1840
+ ?? packageTools?.get(name)
1841
+ ?? this.toolRegistry.get(name);
1602
1842
  if (tool)
1603
1843
  registryTools.push(tool);
1604
1844
  }
@@ -1702,8 +1942,12 @@ export class SessionManager {
1702
1942
  action: async (currentContent) => {
1703
1943
  const latest = this.sessionConfigs.get(sessionId) ?? initialConfig;
1704
1944
  const runtimeContext = extractPromptContent(latest.systemMessage);
1945
+ // Owner-aware copy pick, re-resolved here because this action
1946
+ // runs per compose: with scope shadowing one agent name can be
1947
+ // served by several packages, and this session must get the
1948
+ // copy its OWNER resolves to — not whichever loaded last.
1705
1949
  const activeAgentPrompt = latest.boundAgentName
1706
- ? this.workerDefaults.agentPromptLookup?.[latest.boundAgentName]?.prompt
1950
+ ? pickAgentCopyForOwner(this.workerDefaults.agentPromptLookup?.[latest.boundAgentName], await this._sessionAgentOwnerKey(sessionId))?.prompt
1707
1951
  : undefined;
1708
1952
  const overlay = mergePromptSections([
1709
1953
  activeAgentPrompt,
@@ -1714,6 +1958,23 @@ export class SessionManager {
1714
1958
  },
1715
1959
  };
1716
1960
  }
1961
+ /**
1962
+ * The session owner's identity key for agent-copy shadowing, or null when
1963
+ * the session is ownerless/system or the owner is unreadable (fail-safe:
1964
+ * no key means no user-scope copy applies, never the wrong one).
1965
+ * Rides the inspect-viewer TTL cache — one CMS read per session per TTL.
1966
+ */
1967
+ async _sessionAgentOwnerKey(sessionId) {
1968
+ try {
1969
+ const viewer = await this._resolveInspectViewer(sessionId);
1970
+ if (!viewer?.provider || !viewer?.subject || viewer.isSystemPrincipal)
1971
+ return null;
1972
+ return agentOwnerKey({ provider: viewer.provider, subject: viewer.subject });
1973
+ }
1974
+ catch {
1975
+ return null;
1976
+ }
1977
+ }
1717
1978
  /**
1718
1979
  * Who the inspect tools act as, for one session.
1719
1980
  *
@@ -1814,7 +2075,7 @@ export class SessionManager {
1814
2075
  return false;
1815
2076
  }
1816
2077
  }
1817
- _buildSystemMessage(sessionId, config) {
2078
+ _buildSystemMessage(sessionId, config, sessionOwnerKey = null) {
1818
2079
  const frameworkBase = this.workerDefaults.frameworkBasePrompt ?? this.workerDefaults.systemMessage;
1819
2080
  const boundAgentName = config.boundAgentName;
1820
2081
  const layerKind = config.promptLayering?.kind ?? (boundAgentName ? "app-agent" : undefined);
@@ -1824,7 +2085,7 @@ export class SessionManager {
1824
2085
  ? { tool_instructions: knowledgeToolInstructions, last_instructions: lastInstructions }
1825
2086
  : { last_instructions: lastInstructions };
1826
2087
  const isPilotSwarmSystemAgent = layerKind === "pilotswarm-system-agent";
1827
- const layerManifest = buildEffectivePromptLayers(this.workerDefaults, config);
2088
+ const layerManifest = buildEffectivePromptLayers(this.workerDefaults, config, sessionOwnerKey);
1828
2089
  return composeStructuredSystemMessage({
1829
2090
  frameworkBase,
1830
2091
  appDefault: isPilotSwarmSystemAgent