@compilr-dev/sdk 0.18.11 → 0.20.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.
@@ -13,22 +13,78 @@ import type { ModelTier } from '../models/index.js';
13
13
  import type { ProviderType } from '../config.js';
14
14
  import type { SharedContextManager } from './shared-context.js';
15
15
  import type { CustomAgentDefinition } from './custom-agents.js';
16
+ /**
17
+ * The editable subset of an agent.
18
+ *
19
+ * ⚠️ `id` AND `role` ARE ABSENT BY DESIGN. The id is immutable (see `TeamAgent.id`); the role
20
+ * is structural — it decides the preset an agent was built from, and the editor's five
21
+ * sections do not offer it.
22
+ */
23
+ export interface TeamAgentUpdate {
24
+ displayName?: string;
25
+ mascot?: MascotExpression;
26
+ description?: string;
27
+ systemPromptAddition?: string;
28
+ toolProfile?: TeamAgentConfig['toolProfile'];
29
+ toolFilter?: string[];
30
+ enabledSkills?: string[];
31
+ }
32
+ /** What an edit did, and whether anyone needs telling. */
33
+ export interface AgentUpdateResult {
34
+ /** The underlying agent was dropped and rebuilt, carrying its history across. */
35
+ rebuilt: boolean;
36
+ /**
37
+ * ⚠️ THE CASE THAT HAS TO REACH THE CONVERSATION. The agent may now touch strictly less
38
+ * than it could — and its own history contains it planning to do things it can no longer
39
+ * do. Its next turn will attempt one and get a refusal it cannot account for: a capability
40
+ * change an agent cannot OBSERVE reads to it as a malfunction.
41
+ *
42
+ * Widening is reported too. Nothing breaks, but a record of when an agent gained the
43
+ * ability to write files is worth more than the row it costs.
44
+ */
45
+ toolsNarrowed: boolean;
46
+ previousProfile?: TeamAgentConfig['toolProfile'];
47
+ newProfile?: TeamAgentConfig['toolProfile'];
48
+ }
49
+ /**
50
+ * Did the tool profile get strictly smaller?
51
+ *
52
+ * ⚠️ Decided by whether the profile is READ-ONLY, not by counting tools. A profile with more
53
+ * tools that cannot write is a narrowing in every sense the user cares about, and tool counts
54
+ * shift whenever the registry grows.
55
+ */
56
+ export declare function isNarrowing(before: TeamAgentConfig['toolProfile'], after: TeamAgentConfig['toolProfile']): boolean;
16
57
  /**
17
58
  * TeamAgent wraps an Agent instance with team-specific metadata
18
59
  */
19
60
  export declare class TeamAgent {
20
61
  /**
21
- * Unique identifier (e.g., 'pm', 'arch', 'qa')
62
+ * Unique identifier (e.g., 'pm', 'arch', 'qa').
63
+ *
64
+ * ⚠️ IMMUTABLE, AND ENFORCED HERE RATHER THAN IN THE UI. Thirteen persisted places key off
65
+ * this string — `work_items.owner` (indexed), `work_item_comments.author`,
66
+ * `projects.active_agent`, `file_locks.agent_id`, the conversation file's `agentId`,
67
+ * `agentIds[]`, `activeAgentId` and its `agentHistories` Record, every message's `agentId`,
68
+ * and both sides of every handoff marker.
69
+ *
70
+ * ⚠️ AND ONE THAT CANNOT BE MIGRATED AT ALL: the literal `$handle` is persisted inside
71
+ * message PROSE. Mentions are resolved when typed, not when read, so the raw text is what
72
+ * gets stored. A perfect cascading rename still cannot fix `$arch` in a sentence a user
73
+ * wrote months ago — and rewriting their words to keep a reference alive is worse than not
74
+ * renaming.
75
+ *
76
+ * The rename path is DUPLICATE, which already excludes handle and face because both must
77
+ * be unique. Explicit and reversible, where a cascading rename is neither.
22
78
  */
23
79
  readonly id: string;
24
80
  /**
25
81
  * Display name shown in UI
26
82
  */
27
- readonly displayName: string;
83
+ displayName: string;
28
84
  /**
29
85
  * Mascot expression for visual differentiation
30
86
  */
31
- readonly mascot: MascotExpression;
87
+ mascot: MascotExpression;
32
88
  /**
33
89
  * Role type
34
90
  */
@@ -36,11 +92,11 @@ export declare class TeamAgent {
36
92
  /**
37
93
  * Optional description
38
94
  */
39
- readonly description?: string;
95
+ description?: string;
40
96
  /**
41
97
  * System prompt addition specific to this agent
42
98
  */
43
- readonly systemPromptAddition?: string;
99
+ systemPromptAddition?: string;
44
100
  /**
45
101
  * Use minimal system prompt - skip all modules
46
102
  */
@@ -52,15 +108,15 @@ export declare class TeamAgent {
52
108
  /**
53
109
  * Tool filter (tool names this agent can use)
54
110
  */
55
- readonly toolFilter?: string[];
111
+ toolFilter?: string[];
56
112
  /**
57
113
  * Tool profile name (for display)
58
114
  */
59
- readonly toolProfile?: TeamAgentConfig['toolProfile'];
115
+ toolProfile?: TeamAgentConfig['toolProfile'];
60
116
  /**
61
117
  * Enabled skills (empty = all skills)
62
118
  */
63
- readonly enabledSkills?: string[];
119
+ enabledSkills?: string[];
64
120
  /**
65
121
  * Auto-approve handoffs from this agent without user confirmation
66
122
  */
@@ -122,6 +178,32 @@ export declare class TeamAgent {
122
178
  * @param newTier - The new model tier to use
123
179
  */
124
180
  setModelTier(newTier: ModelTier): void;
181
+ /**
182
+ * Serialise the live agent, keep its history, and drop the instance so the next
183
+ * `initialize()` rebuilds it.
184
+ *
185
+ * ⚠️ EXTRACTED so `setModelTier` and `applyUpdate` cannot drift. Two copies of a
186
+ * history-transfer is exactly the kind of duplication that survives review and then
187
+ * silently loses a conversation when only one of them is fixed.
188
+ *
189
+ * Thinking blocks are stripped: they carry provider-specific signatures (Gemini's
190
+ * `thought_signature`) that do not transfer across a rebuild.
191
+ */
192
+ private stashForRebuild;
193
+ /**
194
+ * Apply an edit to this agent in place.
195
+ *
196
+ * ⚠️ IN PLACE, NOT A REPLACEMENT INSTANCE. Other structures hold `TeamAgent` references —
197
+ * swapping the object in the team's map would leave every one of them pointing at a stale
198
+ * agent, and nothing would report it.
199
+ *
200
+ * Returns whether the change requires the underlying agent to be rebuilt. Metadata
201
+ * (`displayName`, `mascot`, `description`) is read from the roster at render time, so a
202
+ * rename is retroactive and free; anything that feeds agent CONSTRUCTION is not.
203
+ */
204
+ applyUpdate(patch: TeamAgentUpdate): {
205
+ rebuilt: boolean;
206
+ };
125
207
  /**
126
208
  * Initialize the agent with the given factory
127
209
  * This allows the team to control agent creation
@@ -17,12 +17,66 @@ import { getToolsForProfile, generateToolAwarenessPrompt, generateCoordinatorGui
17
17
  function estimateTokens(text) {
18
18
  return Math.ceil(text.length / 4);
19
19
  }
20
+ /**
21
+ * Did the tool profile get strictly smaller?
22
+ *
23
+ * ⚠️ Decided by whether the profile is READ-ONLY, not by counting tools. A profile with more
24
+ * tools that cannot write is a narrowing in every sense the user cares about, and tool counts
25
+ * shift whenever the registry grows.
26
+ */
27
+ export function isNarrowing(before, after) {
28
+ if (before === after)
29
+ return false;
30
+ if (before === undefined || after === undefined)
31
+ return false;
32
+ return !READ_ONLY_PROFILES.has(before) && READ_ONLY_PROFILES.has(after);
33
+ }
34
+ /**
35
+ * The profiles that cannot change files.
36
+ *
37
+ * ⚠️ Kept in step with `PROFILE_INFO.isReadOnly` by a test, not by discipline — this module
38
+ * cannot import tool-config without a cycle.
39
+ */
40
+ const READ_ONLY_PROFILES = new Set([
41
+ 'read-only',
42
+ 'security',
43
+ 'qa',
44
+ 'architect',
45
+ 'planner',
46
+ 'analyst',
47
+ 'designer',
48
+ ]);
49
+ /** Order-insensitive comparison — a reordered skill list is not a change. */
50
+ function sameStrings(a, b) {
51
+ if (a === undefined || b === undefined)
52
+ return a === b;
53
+ if (a.length !== b.length)
54
+ return false;
55
+ const x = [...a].sort();
56
+ const y = [...b].sort();
57
+ return x.every((v, i) => v === y[i]);
58
+ }
20
59
  /**
21
60
  * TeamAgent wraps an Agent instance with team-specific metadata
22
61
  */
23
62
  export class TeamAgent {
24
63
  /**
25
- * Unique identifier (e.g., 'pm', 'arch', 'qa')
64
+ * Unique identifier (e.g., 'pm', 'arch', 'qa').
65
+ *
66
+ * ⚠️ IMMUTABLE, AND ENFORCED HERE RATHER THAN IN THE UI. Thirteen persisted places key off
67
+ * this string — `work_items.owner` (indexed), `work_item_comments.author`,
68
+ * `projects.active_agent`, `file_locks.agent_id`, the conversation file's `agentId`,
69
+ * `agentIds[]`, `activeAgentId` and its `agentHistories` Record, every message's `agentId`,
70
+ * and both sides of every handoff marker.
71
+ *
72
+ * ⚠️ AND ONE THAT CANNOT BE MIGRATED AT ALL: the literal `$handle` is persisted inside
73
+ * message PROSE. Mentions are resolved when typed, not when read, so the raw text is what
74
+ * gets stored. A perfect cascading rename still cannot fix `$arch` in a sentence a user
75
+ * wrote months ago — and rewriting their words to keep a reference alive is worse than not
76
+ * renaming.
77
+ *
78
+ * The rename path is DUPLICATE, which already excludes handle and face because both must
79
+ * be unique. Explicit and reversible, where a cascading rename is neither.
26
80
  */
27
81
  id;
28
82
  /**
@@ -154,27 +208,70 @@ export class TeamAgent {
154
208
  if (newTier === this._modelTier) {
155
209
  return; // No change needed
156
210
  }
157
- // Store current state if agent is initialized
158
- if (this._agent) {
159
- const state = this._agent.serialize();
160
- // Strip thinking blocks from messages - they contain provider-specific
161
- // signatures (e.g., Gemini's thought_signature) that don't transfer
162
- // between sessions or model changes
163
- state.messages = state.messages.map((msg) => {
164
- // Content can be string or ContentBlock[]
165
- if (typeof msg.content === 'string') {
166
- return msg;
167
- }
168
- return {
169
- ...msg,
170
- content: msg.content.filter((block) => block.type !== 'thinking'),
171
- };
172
- });
173
- this.storedState = state;
174
- this._agent = null; // Clear agent so it will be reinitialized
175
- }
211
+ this.stashForRebuild();
176
212
  this._modelTier = newTier;
177
213
  }
214
+ /**
215
+ * Serialise the live agent, keep its history, and drop the instance so the next
216
+ * `initialize()` rebuilds it.
217
+ *
218
+ * ⚠️ EXTRACTED so `setModelTier` and `applyUpdate` cannot drift. Two copies of a
219
+ * history-transfer is exactly the kind of duplication that survives review and then
220
+ * silently loses a conversation when only one of them is fixed.
221
+ *
222
+ * Thinking blocks are stripped: they carry provider-specific signatures (Gemini's
223
+ * `thought_signature`) that do not transfer across a rebuild.
224
+ */
225
+ stashForRebuild() {
226
+ if (!this._agent)
227
+ return;
228
+ const state = this._agent.serialize();
229
+ state.messages = state.messages.map((msg) => {
230
+ if (typeof msg.content === 'string')
231
+ return msg;
232
+ return {
233
+ ...msg,
234
+ content: msg.content.filter((block) => block.type !== 'thinking'),
235
+ };
236
+ });
237
+ this.storedState = state;
238
+ this._agent = null;
239
+ }
240
+ /**
241
+ * Apply an edit to this agent in place.
242
+ *
243
+ * ⚠️ IN PLACE, NOT A REPLACEMENT INSTANCE. Other structures hold `TeamAgent` references —
244
+ * swapping the object in the team's map would leave every one of them pointing at a stale
245
+ * agent, and nothing would report it.
246
+ *
247
+ * Returns whether the change requires the underlying agent to be rebuilt. Metadata
248
+ * (`displayName`, `mascot`, `description`) is read from the roster at render time, so a
249
+ * rename is retroactive and free; anything that feeds agent CONSTRUCTION is not.
250
+ */
251
+ applyUpdate(patch) {
252
+ const needsRebuild = (patch.systemPromptAddition !== undefined &&
253
+ patch.systemPromptAddition !== this.systemPromptAddition) ||
254
+ (patch.toolProfile !== undefined && patch.toolProfile !== this.toolProfile) ||
255
+ (patch.toolFilter !== undefined && !sameStrings(patch.toolFilter, this.toolFilter)) ||
256
+ (patch.enabledSkills !== undefined && !sameStrings(patch.enabledSkills, this.enabledSkills));
257
+ if (patch.displayName !== undefined)
258
+ this.displayName = patch.displayName;
259
+ if (patch.mascot !== undefined)
260
+ this.mascot = patch.mascot;
261
+ if (patch.description !== undefined)
262
+ this.description = patch.description;
263
+ if (patch.systemPromptAddition !== undefined)
264
+ this.systemPromptAddition = patch.systemPromptAddition;
265
+ if (patch.toolProfile !== undefined)
266
+ this.toolProfile = patch.toolProfile;
267
+ if (patch.toolFilter !== undefined)
268
+ this.toolFilter = patch.toolFilter;
269
+ if (patch.enabledSkills !== undefined)
270
+ this.enabledSkills = patch.enabledSkills;
271
+ if (needsRebuild)
272
+ this.stashForRebuild();
273
+ return { rebuilt: needsRebuild };
274
+ }
178
275
  /**
179
276
  * Initialize the agent with the given factory
180
277
  * This allows the team to control agent creation
@@ -14,6 +14,7 @@ import type { ModelTier } from '../models/index.js';
14
14
  import { SharedContextManager } from './shared-context.js';
15
15
  import { ArtifactStore } from './artifacts.js';
16
16
  import type { CustomAgentDefinition } from './custom-agents.js';
17
+ import type { TeamAgentUpdate, AgentUpdateResult } from './team-agent.js';
17
18
  import type { ISessionRegistry } from './interfaces.js';
18
19
  /**
19
20
  * Configuration for creating an AgentTeam
@@ -192,6 +193,20 @@ export declare class AgentTeam {
192
193
  /**
193
194
  * Remove an agent from the team
194
195
  */
196
+ /**
197
+ * Edit an existing agent.
198
+ *
199
+ * ⚠️ THE FIRST WAY TO CHANGE AN AGENT WITHOUT DELETING IT. Until this existed, altering a
200
+ * name, instruction, tool profile or skill set meant `removeAgent` + `addAgent`, which
201
+ * orphans every conversation the agent owned.
202
+ *
203
+ * Returns what the caller needs to decide whether to tell anyone:
204
+ * - `rebuilt` — the underlying agent was dropped and will rebuild on next use, carrying
205
+ * its history across.
206
+ * - `toolsNarrowed` — the agent may now touch strictly LESS than it could. See the note
207
+ * on the return type: this is the case that needs to reach the conversation.
208
+ */
209
+ updateAgent(id: string, patch: TeamAgentUpdate): Promise<AgentUpdateResult>;
195
210
  removeAgent(id: string): boolean;
196
211
  /**
197
212
  * Clear conversation history for all agents in the team
package/dist/team/team.js CHANGED
@@ -11,6 +11,7 @@ import { TeamAgent } from './team-agent.js';
11
11
  import { ROLE_EXPERTISE } from './types.js';
12
12
  import { SharedContextManager } from './shared-context.js';
13
13
  import { ArtifactStore } from './artifacts.js';
14
+ import { isNarrowing } from './team-agent.js';
14
15
  import { resolveAgentIdCollision } from './collision-utils.js';
15
16
  /**
16
17
  * AgentTeam orchestrates multiple persistent agents
@@ -311,6 +312,50 @@ export class AgentTeam {
311
312
  /**
312
313
  * Remove an agent from the team
313
314
  */
315
+ /**
316
+ * Edit an existing agent.
317
+ *
318
+ * ⚠️ THE FIRST WAY TO CHANGE AN AGENT WITHOUT DELETING IT. Until this existed, altering a
319
+ * name, instruction, tool profile or skill set meant `removeAgent` + `addAgent`, which
320
+ * orphans every conversation the agent owned.
321
+ *
322
+ * Returns what the caller needs to decide whether to tell anyone:
323
+ * - `rebuilt` — the underlying agent was dropped and will rebuild on next use, carrying
324
+ * its history across.
325
+ * - `toolsNarrowed` — the agent may now touch strictly LESS than it could. See the note
326
+ * on the return type: this is the case that needs to reach the conversation.
327
+ */
328
+ async updateAgent(id, patch) {
329
+ const agent = this.agents.get(id);
330
+ if (!agent) {
331
+ throw new Error(`Agent '${id}' not found in team`);
332
+ }
333
+ /*
334
+ ⚠️ Guarded here even though `TeamAgentUpdate` has no `id`. A patch arriving over IPC is
335
+ a plain object that TypeScript never saw; the immutability decision has to hold against
336
+ data, not only against callers that compiled.
337
+ */
338
+ if ('id' in patch) {
339
+ throw new Error(`Agent id is immutable — '${id}' cannot be renamed. Duplicate the agent instead: ` +
340
+ 'mention handles are written into stored message text and cannot be migrated.');
341
+ }
342
+ const previousProfile = agent.toolProfile;
343
+ const { rebuilt } = agent.applyUpdate(patch);
344
+ if (rebuilt) {
345
+ // Rebuild now rather than lazily, so a failure surfaces at save time.
346
+ await agent.initialize(this.agentFactory, this._sharedContext);
347
+ }
348
+ // displayName/mascot feed the roster injected into every agent's context.
349
+ this.updateTeamRoster();
350
+ this.updatedAt = new Date();
351
+ this.emit({ type: 'agent:updated', agentId: id });
352
+ return {
353
+ rebuilt,
354
+ toolsNarrowed: isNarrowing(previousProfile, agent.toolProfile),
355
+ previousProfile,
356
+ newProfile: agent.toolProfile,
357
+ };
358
+ }
314
359
  removeAgent(id) {
315
360
  if (id === 'default') {
316
361
  throw new Error('Cannot remove the default agent');
@@ -297,6 +297,9 @@ export const TOOL_GROUPS = {
297
297
  'canvas_validate',
298
298
  'canvas_screenshot',
299
299
  'canvas_guide',
300
+ 'canvas_asset_add',
301
+ 'canvas_asset_list',
302
+ 'canvas_asset_delete',
300
303
  ],
301
304
  readOnly: false,
302
305
  tier: 'meta',
@@ -211,7 +211,7 @@ export interface SerializedTeam {
211
211
  /**
212
212
  * Event types for team operations
213
213
  */
214
- export type TeamEventType = 'agent:added' | 'agent:removed' | 'agent:switched' | 'agent:switch_warning' | 'agent:collision' | 'agent:message' | 'team:checkpoint' | 'team:restored' | 'team:reset';
214
+ export type TeamEventType = 'agent:added' | 'agent:updated' | 'agent:removed' | 'agent:switched' | 'agent:switch_warning' | 'agent:collision' | 'agent:message' | 'team:checkpoint' | 'team:restored' | 'team:reset';
215
215
  /**
216
216
  * Team event payload
217
217
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.18.11",
3
+ "version": "0.20.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",