@compilr-dev/sdk 0.23.1 → 0.24.0

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.
@@ -68,6 +68,8 @@ export interface TeamAgentUpdate {
68
68
  toolProfile?: TeamAgentConfig['toolProfile'];
69
69
  toolFilter?: string[];
70
70
  enabledSkills?: string[];
71
+ /** Which MCP servers this agent may reach. See `TeamAgent.mcpServers`. */
72
+ mcpServers?: string[];
71
73
  }
72
74
  /** What an edit did, and whether anyone needs telling. */
73
75
  export interface AgentUpdateResult {
@@ -185,6 +187,15 @@ export declare class TeamAgent {
185
187
  * from the profile name alone.
186
188
  */
187
189
  customGroups?: string[];
190
+ /**
191
+ * MCP servers this agent may reach. `undefined` = a pre-grants agent, which reaches all;
192
+ * `[]` = granted nothing. See `TeamAgentConfig.mcpServers`.
193
+ *
194
+ * The SDK holds the grant; the HOST enforces it, because the host owns the MCP connection and
195
+ * decides which tools an agent is built with. `grantsMcpServer()` is the shared predicate so
196
+ * the two cannot disagree about what `undefined` means.
197
+ */
198
+ mcpServers?: string[];
188
199
  /**
189
200
  * Enabled skills (empty = all skills)
190
201
  */
@@ -272,6 +283,37 @@ export declare class TeamAgent {
272
283
  * `thought_signature`) that do not transfer across a rebuild.
273
284
  */
274
285
  private stashForRebuild;
286
+ /**
287
+ * A turn is in flight: the host has called `run()`/`stream()` and is awaiting it.
288
+ *
289
+ * ⚠️ HOSTS MUST BRACKET EVERY RUN, IN A `finally`. `TeamAgent` does not own the run call —
290
+ * the host holds the unwrapped `Agent` — so it cannot observe this for itself.
291
+ */
292
+ private turnStartedAt;
293
+ /**
294
+ * How long a turn may be in flight before this stops believing the host.
295
+ *
296
+ * ⚠️ A FLAG THAT ONLY EVER GETS SET IS WORSE THAN THE BUG IT GUARDS. A host that throws past
297
+ * its `endTurn()` would block every edit to that agent for the life of the process, with no
298
+ * way back short of a restart. Going stale bounds that to one window; the edit that slips
299
+ * through after it is the same edit that would have been allowed before this guard existed.
300
+ */
301
+ private static readonly TURN_STALE_AFTER_MS;
302
+ /**
303
+ * Whether this agent may reach an MCP server.
304
+ *
305
+ * ⚠️ THE ONE PLACE `undefined` IS INTERPRETED. Hosts filter the tools they build an agent
306
+ * with, so each one would otherwise re-decide what "never asked" means — and a host that
307
+ * read it as "deny" would silently strip reach from every agent created before grants
308
+ * existed, which is the migration we explicitly chose not to do.
309
+ */
310
+ grantsMcpServer(server: string): boolean;
311
+ /** Mark the start of a turn. Pair with `endTurn()` in a `finally`. */
312
+ beginTurn(): void;
313
+ /** Mark the end of a turn, however it ended — completed, aborted or thrown. */
314
+ endTurn(): void;
315
+ /** Whether a turn is in flight and recent enough to still be believed. */
316
+ get isTurnInFlight(): boolean;
275
317
  /**
276
318
  * Apply an edit to this agent in place.
277
319
  *
@@ -312,7 +354,20 @@ export declare class TeamAgent {
312
354
  * @param agentFactory Factory function to create the agent
313
355
  * @param sharedContext Optional shared context to inject into system prompt
314
356
  */
315
- initialize(agentFactory: (config: Partial<AgentConfig>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean, toolFilter?: string[], modelTier?: ModelTier) => Promise<Agent>, sharedContext?: SharedContextManager): Promise<void>;
357
+ initialize(agentFactory: (config: Partial<AgentConfig>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean, toolFilter?: string[], modelTier?: ModelTier,
358
+ /**
359
+ * Which agent is being built.
360
+ *
361
+ * ⚠️ ADDED BECAUSE THE FACTORY COULD NOT TELL ITS AGENTS APART. Per-agent MCP grants are
362
+ * enforced by the HOST — it owns the connection and decides which tools an agent is
363
+ * constructed with — and it could not, because every parameter here described the
364
+ * configuration and none identified the agent. Desktop's episode recorder had already
365
+ * worked around the same gap by recording every team agent under the literal id
366
+ * 'team-agent'.
367
+ *
368
+ * Optional, so a host that ignores it behaves exactly as before.
369
+ */
370
+ teamAgent?: TeamAgent) => Promise<Agent>, sharedContext?: SharedContextManager): Promise<void>;
316
371
  /**
317
372
  * Set the agent instance directly
318
373
  * Used for the default agent when an agent is created before the team
@@ -203,6 +203,15 @@ export class TeamAgent {
203
203
  * from the profile name alone.
204
204
  */
205
205
  customGroups;
206
+ /**
207
+ * MCP servers this agent may reach. `undefined` = a pre-grants agent, which reaches all;
208
+ * `[]` = granted nothing. See `TeamAgentConfig.mcpServers`.
209
+ *
210
+ * The SDK holds the grant; the HOST enforces it, because the host owns the MCP connection and
211
+ * decides which tools an agent is built with. `grantsMcpServer()` is the shared predicate so
212
+ * the two cannot disagree about what `undefined` means.
213
+ */
214
+ mcpServers;
206
215
  /**
207
216
  * Enabled skills (empty = all skills)
208
217
  */
@@ -261,6 +270,7 @@ export class TeamAgent {
261
270
  this.toolFilter = config.toolFilter;
262
271
  this.toolProfile = config.toolProfile;
263
272
  this.customGroups = config.customGroups;
273
+ this.mcpServers = config.mcpServers;
264
274
  this.enabledSkills = config.enabledSkills;
265
275
  this.personality = config.personality;
266
276
  this.autoApproveHandoff = config.autoApproveHandoff;
@@ -337,6 +347,47 @@ export class TeamAgent {
337
347
  this.storedState = state;
338
348
  this._agent = null;
339
349
  }
350
+ /**
351
+ * A turn is in flight: the host has called `run()`/`stream()` and is awaiting it.
352
+ *
353
+ * ⚠️ HOSTS MUST BRACKET EVERY RUN, IN A `finally`. `TeamAgent` does not own the run call —
354
+ * the host holds the unwrapped `Agent` — so it cannot observe this for itself.
355
+ */
356
+ turnStartedAt = null;
357
+ /**
358
+ * How long a turn may be in flight before this stops believing the host.
359
+ *
360
+ * ⚠️ A FLAG THAT ONLY EVER GETS SET IS WORSE THAN THE BUG IT GUARDS. A host that throws past
361
+ * its `endTurn()` would block every edit to that agent for the life of the process, with no
362
+ * way back short of a restart. Going stale bounds that to one window; the edit that slips
363
+ * through after it is the same edit that would have been allowed before this guard existed.
364
+ */
365
+ static TURN_STALE_AFTER_MS = 10 * 60 * 1000;
366
+ /**
367
+ * Whether this agent may reach an MCP server.
368
+ *
369
+ * ⚠️ THE ONE PLACE `undefined` IS INTERPRETED. Hosts filter the tools they build an agent
370
+ * with, so each one would otherwise re-decide what "never asked" means — and a host that
371
+ * read it as "deny" would silently strip reach from every agent created before grants
372
+ * existed, which is the migration we explicitly chose not to do.
373
+ */
374
+ grantsMcpServer(server) {
375
+ return this.mcpServers === undefined || this.mcpServers.includes(server);
376
+ }
377
+ /** Mark the start of a turn. Pair with `endTurn()` in a `finally`. */
378
+ beginTurn() {
379
+ this.turnStartedAt = Date.now();
380
+ }
381
+ /** Mark the end of a turn, however it ended — completed, aborted or thrown. */
382
+ endTurn() {
383
+ this.turnStartedAt = null;
384
+ }
385
+ /** Whether a turn is in flight and recent enough to still be believed. */
386
+ get isTurnInFlight() {
387
+ if (this.turnStartedAt === null)
388
+ return false;
389
+ return Date.now() - this.turnStartedAt < TeamAgent.TURN_STALE_AFTER_MS;
390
+ }
340
391
  /**
341
392
  * Apply an edit to this agent in place.
342
393
  *
@@ -354,12 +405,33 @@ export class TeamAgent {
354
405
  (patch.personality !== undefined && patch.personality !== this.personality) ||
355
406
  (patch.toolProfile !== undefined && patch.toolProfile !== this.toolProfile) ||
356
407
  (patch.toolFilter !== undefined && !sameStrings(patch.toolFilter, this.toolFilter)) ||
357
- (patch.enabledSkills !== undefined && !sameStrings(patch.enabledSkills, this.enabledSkills));
408
+ (patch.enabledSkills !== undefined &&
409
+ !sameStrings(patch.enabledSkills, this.enabledSkills)) ||
410
+ (patch.mcpServers !== undefined && !sameStrings(patch.mcpServers, this.mcpServers));
358
411
  /*
359
412
  Delegated: `setModelTier` no-ops when the tier is unchanged and stashes for rebuild
360
413
  when it is not. Its `rebuilt` contribution is folded in below.
361
414
  */
362
415
  const tierChanged = patch.modelTier !== undefined && patch.modelTier !== this._modelTier;
416
+ /*
417
+ ⚠️ REFUSED MID-TURN, BEFORE ANYTHING IS MUTATED. A rebuild snapshots the history with
418
+ `serialize()` and drops the instance — but the in-flight `run()` holds its own reference
419
+ and finishes anyway, appending to an agent nobody will read again. Measured: the user
420
+ watches a reply stream in, and the rebuilt agent is handed the snapshot from BEFORE it,
421
+ with no memory of having said it.
422
+
423
+ Not a broken conversation — `setHistory` repairs tool pairing, so nothing 400s. It is
424
+ worse than that: the agent silently forgets a turn the user just watched, and the next
425
+ thing it says is built on a history the user can see is wrong.
426
+
427
+ Only rebuilding edits are refused. A rename or a new face is read from the roster at
428
+ render time and cannot tear anything, so those still apply mid-turn.
429
+ */
430
+ if ((needsRebuild || tierChanged) && this.isTurnInFlight) {
431
+ throw new Error(`Agent '${this.id}' is mid-reply. Changing its prompt, model, tools or skills rebuilds ` +
432
+ `it and would drop the turn it is in the middle of — wait for the reply to finish, ` +
433
+ `or stop it, then save.`);
434
+ }
363
435
  if (patch.modelTier !== undefined)
364
436
  this.setModelTier(patch.modelTier);
365
437
  if (patch.displayName !== undefined)
@@ -372,6 +444,8 @@ export class TeamAgent {
372
444
  this.systemPromptAddition = patch.systemPromptAddition;
373
445
  if (patch.personality !== undefined)
374
446
  this.personality = patch.personality;
447
+ if (patch.mcpServers !== undefined)
448
+ this.mcpServers = patch.mcpServers;
375
449
  /*
376
450
  ⚠️ THE PROFILE IS A LABEL; `toolFilter` IS THE ENFORCEMENT. `initialize()` passes
377
451
  `toolFilter` to the factory and never looks at `toolProfile`, so setting the profile
@@ -501,7 +575,8 @@ export class TeamAgent {
501
575
  }
502
576
  }
503
577
  this._agent = await agentFactory(this.agentConfig, finalSystemPromptAddition || undefined, this.useMinimalSystemPrompt, this.noTools, this.toolFilter, // Pass tool filter to factory
504
- this._modelTier // Pass model tier to factory
578
+ this._modelTier, // Pass model tier to factory
579
+ this // So the host can honour this agent's MCP grants and name it in logs
505
580
  );
506
581
  // Set the team roster as a pin (dynamically re-injected on every LLM call).
507
582
  // This ensures roster updates (agent added/removed) are always visible.
@@ -682,6 +757,7 @@ export class TeamAgent {
682
757
  toolFilter: this.toolFilter,
683
758
  toolProfile: this.toolProfile,
684
759
  customGroups: this.customGroups,
760
+ mcpServers: this.mcpServers,
685
761
  enabledSkills: this.enabledSkills,
686
762
  modelTier: this.modelTier,
687
763
  autoApproveHandoff: this.autoApproveHandoff,
@@ -738,6 +814,7 @@ export class TeamAgent {
738
814
  toolFilter,
739
815
  toolProfile: data.toolProfile,
740
816
  customGroups: data.customGroups,
817
+ mcpServers: data.mcpServers,
741
818
  enabledSkills: data.enabledSkills,
742
819
  modelTier: data.modelTier,
743
820
  autoApproveHandoff: data.autoApproveHandoff,
@@ -34,7 +34,9 @@ export interface AgentTeamConfig {
34
34
  * @param toolFilter - Limit which tools this agent can use
35
35
  * @param modelTier - Model tier for this agent (fast/balanced/powerful)
36
36
  */
37
- agentFactory: (config: Partial<AgentConfig>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean, toolFilter?: string[], modelTier?: ModelTier) => Promise<Agent>;
37
+ agentFactory: (config: Partial<AgentConfig>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean, toolFilter?: string[], modelTier?: ModelTier,
38
+ /** Which agent is being built — see TeamAgent.initialize. */
39
+ teamAgent?: TeamAgent) => Promise<Agent>;
38
40
  /**
39
41
  * Initial agents to add to the team (optional)
40
42
  * If not provided, starts with just the default agent
@@ -137,6 +137,16 @@ export interface TeamAgentConfig {
137
137
  toolProfile?: ToolProfile;
138
138
  /** Groups behind a `custom` profile — see TeamAgent.customGroups. */
139
139
  customGroups?: string[];
140
+ /**
141
+ * MCP servers this agent may reach, by server name.
142
+ *
143
+ * ⚠️ `undefined` AND `[]` MEAN DIFFERENT THINGS, DELIBERATELY. `undefined` is "never asked" —
144
+ * every agent serialized before per-agent grants existed, which reached every server, and
145
+ * still does. `[]` is "asked, and granted nothing", which is what a newly created agent gets.
146
+ * Collapsing them would either strip reach from agents that depend on it or hand every new
147
+ * agent the whole machine.
148
+ */
149
+ mcpServers?: string[];
140
150
  /**
141
151
  * Enabled skills for this agent
142
152
  * Empty array means all skills, non-empty means filtered
@@ -176,6 +186,8 @@ export interface SerializedTeamAgent {
176
186
  toolFilter?: string[];
177
187
  toolProfile?: ToolProfile;
178
188
  customGroups?: string[];
189
+ /** See TeamAgentConfig.mcpServers — undefined means a pre-grants agent, which reaches all. */
190
+ mcpServers?: string[];
179
191
  enabledSkills?: string[];
180
192
  modelTier?: ModelTier;
181
193
  autoApproveHandoff?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",