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.
- package/api/src/http-api-transport.js +26 -7
- package/api/src/protocol.js +16 -7
- package/dist/agent-manager-tools.d.ts.map +1 -1
- package/dist/agent-manager-tools.js +12 -2
- package/dist/agent-manager-tools.js.map +1 -1
- package/dist/agent-package-installer.d.ts +2 -0
- package/dist/agent-package-installer.d.ts.map +1 -1
- package/dist/agent-package-installer.js +1 -0
- package/dist/agent-package-installer.js.map +1 -1
- package/dist/agent-package-service.d.ts +1 -1
- package/dist/agent-package-service.d.ts.map +1 -1
- package/dist/agent-package-service.js +79 -11
- package/dist/agent-package-service.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +117 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +50 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +65 -0
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +4 -7
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +99 -18
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +8 -1
- package/dist/management-client.js.map +1 -1
- package/dist/model-providers.d.ts +142 -6
- package/dist/model-providers.d.ts.map +1 -1
- package/dist/model-providers.js +117 -1
- package/dist/model-providers.js.map +1 -1
- package/dist/session-manager.d.ts +97 -7
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +292 -31
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +43 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +296 -184
- package/dist/session-proxy.js.map +1 -1
- package/dist/web/generated-op-methods.d.ts +34 -5
- package/dist/web/generated-op-methods.d.ts.map +1 -1
- package/dist/web/generated-op-methods.js +2 -0
- package/dist/web/generated-op-methods.js.map +1 -1
- package/dist/worker.d.ts +23 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +189 -17
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
package/dist/session-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
layers.push(
|
|
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
|
-
|
|
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
|
-
/**
|
|
344
|
-
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|