@compilr-dev/sdk 0.22.0 → 0.23.1

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
@@ -13,6 +13,38 @@ 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
+ * Strip the composite a pre-0.23 `fromRole()` stored, so it is not composed a second time.
18
+ *
19
+ * ⚠️ WITHOUT THIS, EVERY ROLE AGENT CREATED BEFORE 0.23 SENDS ITS ROLE PROMPT TWICE. The first
20
+ * cut of this migration skipped role agents, reasoning that an edited stored prompt is
21
+ * indistinguishable from an unedited one so removing anything risks the user's text. True, and
22
+ * beside the point: the generated sections now compose on top of whatever is stored, so leaving
23
+ * the legacy copy in place duplicates it. Measured on a real `$arch`: 13,024 characters instead
24
+ * of ~6,900, with the role header and capability blurb appearing twice each.
25
+ *
26
+ * ⚠️ AND IT CANNOT WORK BY RECONSTRUCTION. The second cut rebuilt the old composite exactly and
27
+ * stripped it on an exact match. That matched 6 of 16 real agents. The capability blurb lists
28
+ * the profile's tool GROUPS as they stood on the day the agent was created — a `qa` agent saved
29
+ * before the profile gained `consult` carries a blurb nine characters shorter than today's, and
30
+ * no amount of care reproduces a list that has since changed.
31
+ *
32
+ * So this anchors on the two ends, which are stable, and ignores the drifting middle entirely:
33
+ *
34
+ * - the FIRST LINE must be the first line of generated content (the role header, or the
35
+ * guidance heading for roles with no role prompt of their own), and
36
+ * - the CURRENT guidance block must appear — it is always last, and unlike the tool blurb it
37
+ * is not derived from mutable configuration.
38
+ *
39
+ * Everything up to the end of that guidance is generated; whatever follows is the user's and is
40
+ * kept. If either anchor is missing the value is returned untouched, so hand-written text and a
41
+ * future rewording both fail safe: the agent keeps its duplication until someone clears the box,
42
+ * and nobody loses text they wrote.
43
+ *
44
+ * @returns the user's own remainder, `undefined` if the stored value was entirely generated, or
45
+ * `stored` unchanged when it is not recognisably a legacy composite.
46
+ */
47
+ export declare function splitLegacyRolePrompt(stored: string, role: AgentRole): string | undefined;
16
48
  /**
17
49
  * The editable subset of an agent.
18
50
  *
@@ -147,6 +179,12 @@ export declare class TeamAgent {
147
179
  * Tool profile name (for display)
148
180
  */
149
181
  toolProfile?: TeamAgentConfig['toolProfile'];
182
+ /**
183
+ * Groups behind a `custom` profile. Needed because the capability blurb is now composed on
184
+ * every build rather than baked once, and a custom profile's blurb cannot be regenerated
185
+ * from the profile name alone.
186
+ */
187
+ customGroups?: string[];
150
188
  /**
151
189
  * Enabled skills (empty = all skills)
152
190
  */
@@ -248,6 +286,25 @@ export declare class TeamAgent {
248
286
  applyUpdate(patch: TeamAgentUpdate): {
249
287
  rebuilt: boolean;
250
288
  };
289
+ /**
290
+ * The generated half of the prompt, rebuilt from CURRENT state on every build.
291
+ *
292
+ * ⚠️ THIS IS WHY IT IS A METHOD AND NOT A STORED STRING. All three constructors used to bake
293
+ * these sections into `systemPromptAddition`, which broke three ways at once:
294
+ *
295
+ * 1. hosts render that field in a textarea, so users were shown generated prose to hand-edit
296
+ * (`fromRole('pm')` alone put 6,321 characters of role prompt in there) and their save
297
+ * froze it as if they had written it;
298
+ * 2. a custom agent's personality was baked in as `Your approach:` while an edit appended
299
+ * `Your manner:` — so editing it added a contradiction instead of changing it;
300
+ * 3. the capability blurb described whatever profile the agent had AT CREATION, so changing
301
+ * the tool profile updated the filter but left the prompt telling the agent it still had
302
+ * tools it no longer had.
303
+ *
304
+ * Composing here fixes all three: nothing generated is ever stored, and every section reflects
305
+ * the state at the moment the agent is built.
306
+ */
307
+ private generatedPromptSections;
251
308
  /**
252
309
  * Initialize the agent with the given factory
253
310
  * This allows the team to control agent creation
@@ -9,8 +9,60 @@
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
+ /**
15
+ * Strip the composite a pre-0.23 `fromRole()` stored, so it is not composed a second time.
16
+ *
17
+ * ⚠️ WITHOUT THIS, EVERY ROLE AGENT CREATED BEFORE 0.23 SENDS ITS ROLE PROMPT TWICE. The first
18
+ * cut of this migration skipped role agents, reasoning that an edited stored prompt is
19
+ * indistinguishable from an unedited one so removing anything risks the user's text. True, and
20
+ * beside the point: the generated sections now compose on top of whatever is stored, so leaving
21
+ * the legacy copy in place duplicates it. Measured on a real `$arch`: 13,024 characters instead
22
+ * of ~6,900, with the role header and capability blurb appearing twice each.
23
+ *
24
+ * ⚠️ AND IT CANNOT WORK BY RECONSTRUCTION. The second cut rebuilt the old composite exactly and
25
+ * stripped it on an exact match. That matched 6 of 16 real agents. The capability blurb lists
26
+ * the profile's tool GROUPS as they stood on the day the agent was created — a `qa` agent saved
27
+ * before the profile gained `consult` carries a blurb nine characters shorter than today's, and
28
+ * no amount of care reproduces a list that has since changed.
29
+ *
30
+ * So this anchors on the two ends, which are stable, and ignores the drifting middle entirely:
31
+ *
32
+ * - the FIRST LINE must be the first line of generated content (the role header, or the
33
+ * guidance heading for roles with no role prompt of their own), and
34
+ * - the CURRENT guidance block must appear — it is always last, and unlike the tool blurb it
35
+ * is not derived from mutable configuration.
36
+ *
37
+ * Everything up to the end of that guidance is generated; whatever follows is the user's and is
38
+ * kept. If either anchor is missing the value is returned untouched, so hand-written text and a
39
+ * future rewording both fail safe: the agent keeps its duplication until someone clears the box,
40
+ * and nobody loses text they wrote.
41
+ *
42
+ * @returns the user's own remainder, `undefined` if the stored value was entirely generated, or
43
+ * `stored` unchanged when it is not recognisably a legacy composite.
44
+ */
45
+ export function splitLegacyRolePrompt(stored, role) {
46
+ /*
47
+ ⚠️ TYPED AS TOTAL, NOT TOTAL AT RUNTIME. `role` is `AgentRole` to the compiler but arrives
48
+ from a JSON file on disk, so it can be any string — a role removed in a later version, or one
49
+ a fixture invented. The explicit `| undefined` is what makes the guard below legitimate
50
+ rather than "always falsy": lint reasons from the type, and the type is a claim about data
51
+ we do not control.
52
+ */
53
+ const byRole = ROLE_METADATA;
54
+ const metadata = byRole[role];
55
+ if (!metadata)
56
+ return stored;
57
+ const guidance = role === 'default' ? generateCoordinatorGuidance() : generateSpecialistGuidance(role);
58
+ const generatedFirstLine = (metadata.defaultSystemPromptAddition || guidance).split('\n')[0];
59
+ if (stored.split('\n')[0] !== generatedFirstLine)
60
+ return stored;
61
+ const at = stored.lastIndexOf(guidance);
62
+ if (at === -1)
63
+ return stored;
64
+ return stored.slice(at + guidance.length).trim() || undefined;
65
+ }
14
66
  /**
15
67
  * Estimate token count from text (approximate: ~4 chars per token)
16
68
  */
@@ -145,6 +197,12 @@ export class TeamAgent {
145
197
  * Tool profile name (for display)
146
198
  */
147
199
  toolProfile;
200
+ /**
201
+ * Groups behind a `custom` profile. Needed because the capability blurb is now composed on
202
+ * every build rather than baked once, and a custom profile's blurb cannot be regenerated
203
+ * from the profile name alone.
204
+ */
205
+ customGroups;
148
206
  /**
149
207
  * Enabled skills (empty = all skills)
150
208
  */
@@ -202,6 +260,7 @@ export class TeamAgent {
202
260
  this.noTools = config.noTools;
203
261
  this.toolFilter = config.toolFilter;
204
262
  this.toolProfile = config.toolProfile;
263
+ this.customGroups = config.customGroups;
205
264
  this.enabledSkills = config.enabledSkills;
206
265
  this.personality = config.personality;
207
266
  this.autoApproveHandoff = config.autoApproveHandoff;
@@ -341,6 +400,48 @@ export class TeamAgent {
341
400
  this.stashForRebuild();
342
401
  return { rebuilt: needsRebuild || tierChanged };
343
402
  }
403
+ /**
404
+ * The generated half of the prompt, rebuilt from CURRENT state on every build.
405
+ *
406
+ * ⚠️ THIS IS WHY IT IS A METHOD AND NOT A STORED STRING. All three constructors used to bake
407
+ * these sections into `systemPromptAddition`, which broke three ways at once:
408
+ *
409
+ * 1. hosts render that field in a textarea, so users were shown generated prose to hand-edit
410
+ * (`fromRole('pm')` alone put 6,321 characters of role prompt in there) and their save
411
+ * froze it as if they had written it;
412
+ * 2. a custom agent's personality was baked in as `Your approach:` while an edit appended
413
+ * `Your manner:` — so editing it added a contradiction instead of changing it;
414
+ * 3. the capability blurb described whatever profile the agent had AT CREATION, so changing
415
+ * the tool profile updated the filter but left the prompt telling the agent it still had
416
+ * tools it no longer had.
417
+ *
418
+ * Composing here fixes all three: nothing generated is ever stored, and every section reflects
419
+ * the state at the moment the agent is built.
420
+ */
421
+ generatedPromptSections() {
422
+ const sections = [];
423
+ if (this.role === 'custom') {
424
+ sections.push(generateCustomAgentScaffold(this.displayName, this.description ?? ''));
425
+ }
426
+ else {
427
+ const roleScaffold = ROLE_METADATA[this.role].defaultSystemPromptAddition;
428
+ if (roleScaffold)
429
+ sections.push(roleScaffold);
430
+ }
431
+ // Capability blurb — from the live profile, never a baked copy (defect 3 above).
432
+ const profile = this.toolProfile;
433
+ if (profile && profile !== 'full') {
434
+ sections.push(generateToolAwarenessPrompt({ profile, customGroups: this.customGroups }));
435
+ }
436
+ // Delegation guidance. Custom agents never had it and still do not — this method reproduces
437
+ // what each constructor built, it does not redistribute behaviour between roles.
438
+ if (this.role !== 'custom') {
439
+ sections.push(this.role === 'default'
440
+ ? generateCoordinatorGuidance()
441
+ : generateSpecialistGuidance(this.role));
442
+ }
443
+ return sections;
444
+ }
344
445
  /**
345
446
  * Initialize the agent with the given factory
346
447
  * This allows the team to control agent creation
@@ -352,8 +453,13 @@ export class TeamAgent {
352
453
  if (this._agent) {
353
454
  return; // Already initialized
354
455
  }
355
- // Build system prompt addition with shared context
356
- let finalSystemPromptAddition = this.systemPromptAddition ?? '';
456
+ /*
457
+ Generated sections first, then the user's own words. `systemPromptAddition` holds ONLY what
458
+ the user typed — see `generatedPromptSections()` for why nothing generated is stored.
459
+ */
460
+ let finalSystemPromptAddition = [...this.generatedPromptSections(), this.systemPromptAddition]
461
+ .filter((section) => Boolean(section?.trim()))
462
+ .join('\n\n');
357
463
  // Identity preamble — every team agent gets a stable self-reference
358
464
  // block at the top of its prompt addition. Without this, the agent
359
465
  // knows it's "the PM" from the role prompt but doesn't connect that
@@ -575,6 +681,7 @@ export class TeamAgent {
575
681
  noTools: this.noTools,
576
682
  toolFilter: this.toolFilter,
577
683
  toolProfile: this.toolProfile,
684
+ customGroups: this.customGroups,
578
685
  enabledSkills: this.enabledSkills,
579
686
  modelTier: this.modelTier,
580
687
  autoApproveHandoff: this.autoApproveHandoff,
@@ -588,18 +695,49 @@ export class TeamAgent {
588
695
  // Refresh tool filter from profile if available (ensures new tools are picked up)
589
696
  // This handles the case where tools were added after the agent was created
590
697
  const toolFilter = data.toolProfile ? getToolsForProfile(data.toolProfile) : data.toolFilter;
698
+ /*
699
+ MIGRATION — agents serialized before generated text was separated out.
700
+ Their `systemPromptAddition` holds the whole generated composite, because that is what the
701
+ constructors used to store. Recover the parts that are genuinely the user's.
702
+
703
+ ⚠️ THE FAILURE MODE IS "CHANGE NOTHING", NEVER "GUESS". `splitLegacyCustomPrompt` returns an
704
+ empty result for anything it does not positively recognise, and we then keep the stored
705
+ string exactly as it is — which is the pre-migration behaviour. A generator that has since
706
+ changed wording costs one agent a stale prompt until its next edit; a clever guess would
707
+ cost the user the instructions they wrote.
708
+
709
+ Role agents are deliberately NOT migrated. Their stored string is generated in full, but an
710
+ edited one is indistinguishable from an unedited one, so anything removed might be the
711
+ user's. Their generated sections now compose on top of whatever is stored: a legacy role
712
+ agent shows its old prompt in the editor until someone clears it, and nothing is lost.
713
+ */
714
+ let systemPromptAddition = data.systemPromptAddition;
715
+ let personality = data.personality;
716
+ if (data.role === 'custom' && systemPromptAddition) {
717
+ const legacy = splitLegacyCustomPrompt(systemPromptAddition);
718
+ if (legacy.userText !== undefined || legacy.personality !== undefined) {
719
+ systemPromptAddition = legacy.userText;
720
+ // Only rescue the baked personality when the live field never held one — a value set
721
+ // through the editor is newer than anything frozen in the prompt.
722
+ personality = personality ?? legacy.personality;
723
+ }
724
+ }
725
+ else if (data.role !== 'custom' && systemPromptAddition) {
726
+ systemPromptAddition = splitLegacyRolePrompt(systemPromptAddition, data.role);
727
+ }
591
728
  const agent = new TeamAgent({
592
729
  id: data.id,
593
730
  displayName: data.displayName,
594
731
  mascot: data.mascot,
595
732
  role: data.role,
596
733
  description: data.description,
597
- systemPromptAddition: data.systemPromptAddition,
598
- personality: data.personality,
734
+ systemPromptAddition,
735
+ personality,
599
736
  useMinimalSystemPrompt: data.useMinimalSystemPrompt,
600
737
  noTools: data.noTools,
601
738
  toolFilter,
602
739
  toolProfile: data.toolProfile,
740
+ customGroups: data.customGroups,
603
741
  enabledSkills: data.enabledSkills,
604
742
  modelTier: data.modelTier,
605
743
  autoApproveHandoff: data.autoApproveHandoff,
@@ -619,36 +757,14 @@ export class TeamAgent {
619
757
  // Get tool filter from default profile (if specified)
620
758
  const profile = metadata.defaultToolProfile;
621
759
  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
- }
760
+ // The role prompt, capability blurb and delegation guidance are all generated, so they are
761
+ // composed at initialize() rather than stored. A fresh role agent has no user text at all.
645
762
  return new TeamAgent({
646
763
  id: id ?? role,
647
764
  displayName: metadata.displayName,
648
765
  mascot: metadata.mascot,
649
766
  role,
650
767
  description: metadata.description,
651
- systemPromptAddition: systemPromptAddition || undefined,
652
768
  useMinimalSystemPrompt: metadata.useMinimalSystemPrompt,
653
769
  noTools: metadata.noTools,
654
770
  toolFilter,
@@ -667,31 +783,15 @@ export class TeamAgent {
667
783
  const metadata = ROLE_METADATA[baseRole];
668
784
  const profile = opts.toolProfile ?? metadata.defaultToolProfile;
669
785
  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;
786
+ // Only the specialist's own addition is authored text; the role prompt, capability blurb and
787
+ // collaboration guidance are generated and composed at initialize().
688
788
  return new TeamAgent({
689
789
  id: opts.id ?? baseRole,
690
790
  displayName: opts.displayName ?? metadata.displayName,
691
791
  mascot: metadata.mascot,
692
792
  role: baseRole,
693
793
  description: metadata.description,
694
- systemPromptAddition: systemPromptAddition || undefined,
794
+ systemPromptAddition: opts.systemPromptAddition?.trim() || undefined,
695
795
  useMinimalSystemPrompt: metadata.useMinimalSystemPrompt,
696
796
  noTools: metadata.noTools,
697
797
  toolFilter,
@@ -711,9 +811,16 @@ export class TeamAgent {
711
811
  mascot: def.mascot,
712
812
  role: 'custom',
713
813
  description: def.specialty,
714
- systemPromptAddition: generateCustomAgentSystemPrompt(def),
814
+ // The user's own words only — the identity and focus lines are composed at initialize().
815
+ systemPromptAddition: def.systemPromptAddition,
816
+ /* ⚠️ WITHOUT THIS an edit ADDS a second personality instead of changing the one in force:
817
+ the definition's value was baked into the prompt as `Your approach:` and never reached
818
+ the live field, so `updateAgent({ personality })` appended a contradicting `Your manner:`
819
+ while the original stayed active. */
820
+ personality: def.personality,
715
821
  toolFilter, // Pass tool filter from custom agent config
716
822
  toolProfile: def.toolConfig?.profile, // Store profile for display
823
+ customGroups: def.toolConfig?.customGroups,
717
824
  enabledSkills: def.enabledSkills, // Store skills for display
718
825
  modelTier: def.modelTier, // Store model tier
719
826
  });
@@ -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.1",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",