@compilr-dev/sdk 0.22.0 → 0.23.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.
@@ -33,6 +33,36 @@ export declare const CUSTOM_MASCOTS: string[];
33
33
  * Avoids mascots already in use by other custom agents.
34
34
  */
35
35
  export declare function assignMascot(existingAgents: CustomAgentDefinition[]): string;
36
+ /**
37
+ * The generated scaffold for a custom agent: who it is and what it sticks to.
38
+ *
39
+ * ⚠️ COMPOSED AT `TeamAgent.initialize()`, NEVER STORED in `systemPromptAddition`. That field
40
+ * is the user's own words and nothing else — a host that renders it in a textarea (both of ours
41
+ * do) must not be shown machine-written prose to hand-edit, and must not freeze it on save.
42
+ */
43
+ export declare function generateCustomAgentScaffold(displayName: string, specialty: string): string;
44
+ /** What a legacy composite yielded up: the user's own words, and a personality to rescue. */
45
+ export interface LegacyCustomPromptParts {
46
+ /** The text the user actually typed, or undefined if they never typed any. */
47
+ userText?: string;
48
+ /** Personality recovered from a frozen `Your approach:` line — see below. */
49
+ personality?: string;
50
+ }
51
+ /**
52
+ * Split a pre-0.23 custom-agent composite back into the parts that belong to the user.
53
+ *
54
+ * Before this change `fromCustomDefinition` stored the whole generated composite in
55
+ * `systemPromptAddition`, so every agent already on disk has it frozen there. This reverses that
56
+ * for the fields we can recover without guessing.
57
+ *
58
+ * ⚠️ `personality` is recovered, not discarded. Legacy `fromCustomDefinition` never passed the
59
+ * definition's personality to the live agent, so for these agents the ONLY surviving copy is the
60
+ * `Your approach:` line inside this string. Dropping it would silently delete a value the user set.
61
+ *
62
+ * Returns `{}` when the string does not look like a composite, which is the signal to leave the
63
+ * stored value exactly as it is. Guessing is worse than migrating nothing.
64
+ */
65
+ export declare function splitLegacyCustomPrompt(stored: string): LegacyCustomPromptParts;
36
66
  /**
37
67
  * Generate system prompt for a custom agent.
38
68
  * Uses a template-based approach (no LLM call).
@@ -49,6 +49,57 @@ export function assignMascot(existingAgents) {
49
49
  // =============================================================================
50
50
  // System Prompt Generation
51
51
  // =============================================================================
52
+ /**
53
+ * The generated scaffold for a custom agent: who it is and what it sticks to.
54
+ *
55
+ * ⚠️ COMPOSED AT `TeamAgent.initialize()`, NEVER STORED in `systemPromptAddition`. That field
56
+ * is the user's own words and nothing else — a host that renders it in a textarea (both of ours
57
+ * do) must not be shown machine-written prose to hand-edit, and must not freeze it on save.
58
+ */
59
+ export function generateCustomAgentScaffold(displayName, specialty) {
60
+ return [
61
+ `You are a ${displayName} specialized in ${specialty}.`,
62
+ '',
63
+ 'Focus on your area of expertise. When questions fall outside your specialty, suggest which team member might be better suited to help.',
64
+ ].join('\n');
65
+ }
66
+ /**
67
+ * Split a pre-0.23 custom-agent composite back into the parts that belong to the user.
68
+ *
69
+ * Before this change `fromCustomDefinition` stored the whole generated composite in
70
+ * `systemPromptAddition`, so every agent already on disk has it frozen there. This reverses that
71
+ * for the fields we can recover without guessing.
72
+ *
73
+ * ⚠️ `personality` is recovered, not discarded. Legacy `fromCustomDefinition` never passed the
74
+ * definition's personality to the live agent, so for these agents the ONLY surviving copy is the
75
+ * `Your approach:` line inside this string. Dropping it would silently delete a value the user set.
76
+ *
77
+ * Returns `{}` when the string does not look like a composite, which is the signal to leave the
78
+ * stored value exactly as it is. Guessing is worse than migrating nothing.
79
+ */
80
+ export function splitLegacyCustomPrompt(stored) {
81
+ // The identity line is what makes this recognisable as generated. No line, no migration.
82
+ if (!/^You are a .+ specialized in .+\.$/m.test(stored))
83
+ return {};
84
+ const parts = {};
85
+ const approach = /^Your approach: (.+)$/m.exec(stored);
86
+ if (approach)
87
+ parts.personality = approach[1].trim();
88
+ const marker = '## Custom Instructions\n\n';
89
+ const at = stored.indexOf(marker);
90
+ if (at !== -1) {
91
+ let body = stored.slice(at + marker.length);
92
+ // Tool awareness, when present, was appended after a `---` rule. Everything from there on
93
+ // is generated too.
94
+ const rule = body.indexOf('\n\n---\n\n');
95
+ if (rule !== -1)
96
+ body = body.slice(0, rule);
97
+ const trimmed = body.trim();
98
+ if (trimmed)
99
+ parts.userText = trimmed;
100
+ }
101
+ return parts;
102
+ }
52
103
  /**
53
104
  * Generate system prompt for a custom agent.
54
105
  * Uses a template-based approach (no LLM call).
@@ -56,10 +107,6 @@ export function assignMascot(existingAgents) {
56
107
  */
57
108
  export function generateCustomAgentSystemPrompt(agent) {
58
109
  const lines = [`You are a ${agent.displayName} specialized in ${agent.specialty}.`];
59
- if (agent.personality) {
60
- lines.push('');
61
- lines.push(`Your approach: ${agent.personality}`);
62
- }
63
110
  lines.push('');
64
111
  lines.push('Focus on your area of expertise. When questions fall outside your specialty, suggest which team member might be better suited to help.');
65
112
  // Append custom instructions if provided
@@ -147,6 +147,12 @@ export declare class TeamAgent {
147
147
  * Tool profile name (for display)
148
148
  */
149
149
  toolProfile?: TeamAgentConfig['toolProfile'];
150
+ /**
151
+ * Groups behind a `custom` profile. Needed because the capability blurb is now composed on
152
+ * every build rather than baked once, and a custom profile's blurb cannot be regenerated
153
+ * from the profile name alone.
154
+ */
155
+ customGroups?: string[];
150
156
  /**
151
157
  * Enabled skills (empty = all skills)
152
158
  */
@@ -248,6 +254,25 @@ export declare class TeamAgent {
248
254
  applyUpdate(patch: TeamAgentUpdate): {
249
255
  rebuilt: boolean;
250
256
  };
257
+ /**
258
+ * The generated half of the prompt, rebuilt from CURRENT state on every build.
259
+ *
260
+ * ⚠️ THIS IS WHY IT IS A METHOD AND NOT A STORED STRING. All three constructors used to bake
261
+ * these sections into `systemPromptAddition`, which broke three ways at once:
262
+ *
263
+ * 1. hosts render that field in a textarea, so users were shown generated prose to hand-edit
264
+ * (`fromRole('pm')` alone put 6,321 characters of role prompt in there) and their save
265
+ * froze it as if they had written it;
266
+ * 2. a custom agent's personality was baked in as `Your approach:` while an edit appended
267
+ * `Your manner:` — so editing it added a contradiction instead of changing it;
268
+ * 3. the capability blurb described whatever profile the agent had AT CREATION, so changing
269
+ * the tool profile updated the filter but left the prompt telling the agent it still had
270
+ * tools it no longer had.
271
+ *
272
+ * Composing here fixes all three: nothing generated is ever stored, and every section reflects
273
+ * the state at the moment the agent is built.
274
+ */
275
+ private generatedPromptSections;
251
276
  /**
252
277
  * Initialize the agent with the given factory
253
278
  * This allows the team to control agent creation
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { ROLE_METADATA } from './types.js';
11
11
  import { getModelForTier, getModelContextWindow } from '../models/index.js';
12
- import { generateCustomAgentSystemPrompt, getCustomAgentToolFilter } from './custom-agents.js';
12
+ import { generateCustomAgentScaffold, getCustomAgentToolFilter, splitLegacyCustomPrompt, } from './custom-agents.js';
13
13
  import { getToolsForProfile, PROFILE_INFO, generateToolAwarenessPrompt, generateCoordinatorGuidance, generateSpecialistGuidance, } from './tool-config.js';
14
14
  /**
15
15
  * Estimate token count from text (approximate: ~4 chars per token)
@@ -145,6 +145,12 @@ export class TeamAgent {
145
145
  * Tool profile name (for display)
146
146
  */
147
147
  toolProfile;
148
+ /**
149
+ * Groups behind a `custom` profile. Needed because the capability blurb is now composed on
150
+ * every build rather than baked once, and a custom profile's blurb cannot be regenerated
151
+ * from the profile name alone.
152
+ */
153
+ customGroups;
148
154
  /**
149
155
  * Enabled skills (empty = all skills)
150
156
  */
@@ -202,6 +208,7 @@ export class TeamAgent {
202
208
  this.noTools = config.noTools;
203
209
  this.toolFilter = config.toolFilter;
204
210
  this.toolProfile = config.toolProfile;
211
+ this.customGroups = config.customGroups;
205
212
  this.enabledSkills = config.enabledSkills;
206
213
  this.personality = config.personality;
207
214
  this.autoApproveHandoff = config.autoApproveHandoff;
@@ -341,6 +348,48 @@ export class TeamAgent {
341
348
  this.stashForRebuild();
342
349
  return { rebuilt: needsRebuild || tierChanged };
343
350
  }
351
+ /**
352
+ * The generated half of the prompt, rebuilt from CURRENT state on every build.
353
+ *
354
+ * ⚠️ THIS IS WHY IT IS A METHOD AND NOT A STORED STRING. All three constructors used to bake
355
+ * these sections into `systemPromptAddition`, which broke three ways at once:
356
+ *
357
+ * 1. hosts render that field in a textarea, so users were shown generated prose to hand-edit
358
+ * (`fromRole('pm')` alone put 6,321 characters of role prompt in there) and their save
359
+ * froze it as if they had written it;
360
+ * 2. a custom agent's personality was baked in as `Your approach:` while an edit appended
361
+ * `Your manner:` — so editing it added a contradiction instead of changing it;
362
+ * 3. the capability blurb described whatever profile the agent had AT CREATION, so changing
363
+ * the tool profile updated the filter but left the prompt telling the agent it still had
364
+ * tools it no longer had.
365
+ *
366
+ * Composing here fixes all three: nothing generated is ever stored, and every section reflects
367
+ * the state at the moment the agent is built.
368
+ */
369
+ generatedPromptSections() {
370
+ const sections = [];
371
+ if (this.role === 'custom') {
372
+ sections.push(generateCustomAgentScaffold(this.displayName, this.description ?? ''));
373
+ }
374
+ else {
375
+ const roleScaffold = ROLE_METADATA[this.role].defaultSystemPromptAddition;
376
+ if (roleScaffold)
377
+ sections.push(roleScaffold);
378
+ }
379
+ // Capability blurb — from the live profile, never a baked copy (defect 3 above).
380
+ const profile = this.toolProfile;
381
+ if (profile && profile !== 'full') {
382
+ sections.push(generateToolAwarenessPrompt({ profile, customGroups: this.customGroups }));
383
+ }
384
+ // Delegation guidance. Custom agents never had it and still do not — this method reproduces
385
+ // what each constructor built, it does not redistribute behaviour between roles.
386
+ if (this.role !== 'custom') {
387
+ sections.push(this.role === 'default'
388
+ ? generateCoordinatorGuidance()
389
+ : generateSpecialistGuidance(this.role));
390
+ }
391
+ return sections;
392
+ }
344
393
  /**
345
394
  * Initialize the agent with the given factory
346
395
  * This allows the team to control agent creation
@@ -352,8 +401,13 @@ export class TeamAgent {
352
401
  if (this._agent) {
353
402
  return; // Already initialized
354
403
  }
355
- // Build system prompt addition with shared context
356
- let finalSystemPromptAddition = this.systemPromptAddition ?? '';
404
+ /*
405
+ Generated sections first, then the user's own words. `systemPromptAddition` holds ONLY what
406
+ the user typed — see `generatedPromptSections()` for why nothing generated is stored.
407
+ */
408
+ let finalSystemPromptAddition = [...this.generatedPromptSections(), this.systemPromptAddition]
409
+ .filter((section) => Boolean(section?.trim()))
410
+ .join('\n\n');
357
411
  // Identity preamble — every team agent gets a stable self-reference
358
412
  // block at the top of its prompt addition. Without this, the agent
359
413
  // knows it's "the PM" from the role prompt but doesn't connect that
@@ -575,6 +629,7 @@ export class TeamAgent {
575
629
  noTools: this.noTools,
576
630
  toolFilter: this.toolFilter,
577
631
  toolProfile: this.toolProfile,
632
+ customGroups: this.customGroups,
578
633
  enabledSkills: this.enabledSkills,
579
634
  modelTier: this.modelTier,
580
635
  autoApproveHandoff: this.autoApproveHandoff,
@@ -588,18 +643,46 @@ export class TeamAgent {
588
643
  // Refresh tool filter from profile if available (ensures new tools are picked up)
589
644
  // This handles the case where tools were added after the agent was created
590
645
  const toolFilter = data.toolProfile ? getToolsForProfile(data.toolProfile) : data.toolFilter;
646
+ /*
647
+ MIGRATION — agents serialized before generated text was separated out.
648
+ Their `systemPromptAddition` holds the whole generated composite, because that is what the
649
+ constructors used to store. Recover the parts that are genuinely the user's.
650
+
651
+ ⚠️ THE FAILURE MODE IS "CHANGE NOTHING", NEVER "GUESS". `splitLegacyCustomPrompt` returns an
652
+ empty result for anything it does not positively recognise, and we then keep the stored
653
+ string exactly as it is — which is the pre-migration behaviour. A generator that has since
654
+ changed wording costs one agent a stale prompt until its next edit; a clever guess would
655
+ cost the user the instructions they wrote.
656
+
657
+ Role agents are deliberately NOT migrated. Their stored string is generated in full, but an
658
+ edited one is indistinguishable from an unedited one, so anything removed might be the
659
+ user's. Their generated sections now compose on top of whatever is stored: a legacy role
660
+ agent shows its old prompt in the editor until someone clears it, and nothing is lost.
661
+ */
662
+ let systemPromptAddition = data.systemPromptAddition;
663
+ let personality = data.personality;
664
+ if (data.role === 'custom' && systemPromptAddition) {
665
+ const legacy = splitLegacyCustomPrompt(systemPromptAddition);
666
+ if (legacy.userText !== undefined || legacy.personality !== undefined) {
667
+ systemPromptAddition = legacy.userText;
668
+ // Only rescue the baked personality when the live field never held one — a value set
669
+ // through the editor is newer than anything frozen in the prompt.
670
+ personality = personality ?? legacy.personality;
671
+ }
672
+ }
591
673
  const agent = new TeamAgent({
592
674
  id: data.id,
593
675
  displayName: data.displayName,
594
676
  mascot: data.mascot,
595
677
  role: data.role,
596
678
  description: data.description,
597
- systemPromptAddition: data.systemPromptAddition,
598
- personality: data.personality,
679
+ systemPromptAddition,
680
+ personality,
599
681
  useMinimalSystemPrompt: data.useMinimalSystemPrompt,
600
682
  noTools: data.noTools,
601
683
  toolFilter,
602
684
  toolProfile: data.toolProfile,
685
+ customGroups: data.customGroups,
603
686
  enabledSkills: data.enabledSkills,
604
687
  modelTier: data.modelTier,
605
688
  autoApproveHandoff: data.autoApproveHandoff,
@@ -619,36 +702,14 @@ export class TeamAgent {
619
702
  // Get tool filter from default profile (if specified)
620
703
  const profile = metadata.defaultToolProfile;
621
704
  const toolFilter = profile ? getToolsForProfile(profile) : undefined;
622
- // Build system prompt with tool awareness (for non-full profiles)
623
- let systemPromptAddition = metadata.defaultSystemPromptAddition ?? '';
624
- if (profile && profile !== 'full') {
625
- const toolAwareness = generateToolAwarenessPrompt({ profile });
626
- systemPromptAddition = systemPromptAddition
627
- ? `${systemPromptAddition}\n\n${toolAwareness}`
628
- : toolAwareness;
629
- }
630
- // Add delegation guidance based on role
631
- if (role === 'default') {
632
- // Coordinator gets delegation tool guidance
633
- const coordinatorGuidance = generateCoordinatorGuidance();
634
- systemPromptAddition = systemPromptAddition
635
- ? `${systemPromptAddition}\n\n${coordinatorGuidance}`
636
- : coordinatorGuidance;
637
- }
638
- else {
639
- // Specialists get collaboration guidance
640
- const specialistGuidance = generateSpecialistGuidance(role);
641
- systemPromptAddition = systemPromptAddition
642
- ? `${systemPromptAddition}\n\n${specialistGuidance}`
643
- : specialistGuidance;
644
- }
705
+ // The role prompt, capability blurb and delegation guidance are all generated, so they are
706
+ // composed at initialize() rather than stored. A fresh role agent has no user text at all.
645
707
  return new TeamAgent({
646
708
  id: id ?? role,
647
709
  displayName: metadata.displayName,
648
710
  mascot: metadata.mascot,
649
711
  role,
650
712
  description: metadata.description,
651
- systemPromptAddition: systemPromptAddition || undefined,
652
713
  useMinimalSystemPrompt: metadata.useMinimalSystemPrompt,
653
714
  noTools: metadata.noTools,
654
715
  toolFilter,
@@ -667,31 +728,15 @@ export class TeamAgent {
667
728
  const metadata = ROLE_METADATA[baseRole];
668
729
  const profile = opts.toolProfile ?? metadata.defaultToolProfile;
669
730
  const toolFilter = profile ? getToolsForProfile(profile) : undefined;
670
- // Base role prompt + the specialist addition (specialization on top of role).
671
- let systemPromptAddition = metadata.defaultSystemPromptAddition ?? '';
672
- if (opts.systemPromptAddition?.trim()) {
673
- systemPromptAddition = systemPromptAddition
674
- ? `${systemPromptAddition}\n\n${opts.systemPromptAddition.trim()}`
675
- : opts.systemPromptAddition.trim();
676
- }
677
- if (profile && profile !== 'full') {
678
- const toolAwareness = generateToolAwarenessPrompt({ profile });
679
- systemPromptAddition = systemPromptAddition
680
- ? `${systemPromptAddition}\n\n${toolAwareness}`
681
- : toolAwareness;
682
- }
683
- // Specialists always get collaboration guidance (never the coordinator path).
684
- const specialistGuidance = generateSpecialistGuidance(baseRole);
685
- systemPromptAddition = systemPromptAddition
686
- ? `${systemPromptAddition}\n\n${specialistGuidance}`
687
- : specialistGuidance;
731
+ // Only the specialist's own addition is authored text; the role prompt, capability blurb and
732
+ // collaboration guidance are generated and composed at initialize().
688
733
  return new TeamAgent({
689
734
  id: opts.id ?? baseRole,
690
735
  displayName: opts.displayName ?? metadata.displayName,
691
736
  mascot: metadata.mascot,
692
737
  role: baseRole,
693
738
  description: metadata.description,
694
- systemPromptAddition: systemPromptAddition || undefined,
739
+ systemPromptAddition: opts.systemPromptAddition?.trim() || undefined,
695
740
  useMinimalSystemPrompt: metadata.useMinimalSystemPrompt,
696
741
  noTools: metadata.noTools,
697
742
  toolFilter,
@@ -711,9 +756,16 @@ export class TeamAgent {
711
756
  mascot: def.mascot,
712
757
  role: 'custom',
713
758
  description: def.specialty,
714
- systemPromptAddition: generateCustomAgentSystemPrompt(def),
759
+ // The user's own words only — the identity and focus lines are composed at initialize().
760
+ systemPromptAddition: def.systemPromptAddition,
761
+ /* ⚠️ WITHOUT THIS an edit ADDS a second personality instead of changing the one in force:
762
+ the definition's value was baked into the prompt as `Your approach:` and never reached
763
+ the live field, so `updateAgent({ personality })` appended a contradicting `Your manner:`
764
+ while the original stayed active. */
765
+ personality: def.personality,
715
766
  toolFilter, // Pass tool filter from custom agent config
716
767
  toolProfile: def.toolConfig?.profile, // Store profile for display
768
+ customGroups: def.toolConfig?.customGroups,
717
769
  enabledSkills: def.enabledSkills, // Store skills for display
718
770
  modelTier: def.modelTier, // Store model tier
719
771
  });
@@ -135,6 +135,8 @@ export interface TeamAgentConfig {
135
135
  * Shows which profile was selected when creating the agent
136
136
  */
137
137
  toolProfile?: ToolProfile;
138
+ /** Groups behind a `custom` profile — see TeamAgent.customGroups. */
139
+ customGroups?: string[];
138
140
  /**
139
141
  * Enabled skills for this agent
140
142
  * Empty array means all skills, non-empty means filtered
@@ -173,6 +175,7 @@ export interface SerializedTeamAgent {
173
175
  noTools?: boolean;
174
176
  toolFilter?: string[];
175
177
  toolProfile?: ToolProfile;
178
+ customGroups?: string[];
176
179
  enabledSkills?: string[];
177
180
  modelTier?: ModelTier;
178
181
  autoApproveHandoff?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",