@compilr-dev/sdk 0.23.0 → 0.23.2

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,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
  *
@@ -240,6 +272,28 @@ export declare class TeamAgent {
240
272
  * `thought_signature`) that do not transfer across a rebuild.
241
273
  */
242
274
  private stashForRebuild;
275
+ /**
276
+ * A turn is in flight: the host has called `run()`/`stream()` and is awaiting it.
277
+ *
278
+ * ⚠️ HOSTS MUST BRACKET EVERY RUN, IN A `finally`. `TeamAgent` does not own the run call —
279
+ * the host holds the unwrapped `Agent` — so it cannot observe this for itself.
280
+ */
281
+ private turnStartedAt;
282
+ /**
283
+ * How long a turn may be in flight before this stops believing the host.
284
+ *
285
+ * ⚠️ A FLAG THAT ONLY EVER GETS SET IS WORSE THAN THE BUG IT GUARDS. A host that throws past
286
+ * its `endTurn()` would block every edit to that agent for the life of the process, with no
287
+ * way back short of a restart. Going stale bounds that to one window; the edit that slips
288
+ * through after it is the same edit that would have been allowed before this guard existed.
289
+ */
290
+ private static readonly TURN_STALE_AFTER_MS;
291
+ /** Mark the start of a turn. Pair with `endTurn()` in a `finally`. */
292
+ beginTurn(): void;
293
+ /** Mark the end of a turn, however it ended — completed, aborted or thrown. */
294
+ endTurn(): void;
295
+ /** Whether a turn is in flight and recent enough to still be believed. */
296
+ get isTurnInFlight(): boolean;
243
297
  /**
244
298
  * Apply an edit to this agent in place.
245
299
  *
@@ -11,6 +11,58 @@ import { ROLE_METADATA } from './types.js';
11
11
  import { getModelForTier, getModelContextWindow } from '../models/index.js';
12
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
  */
@@ -285,6 +337,36 @@ export class TeamAgent {
285
337
  this.storedState = state;
286
338
  this._agent = null;
287
339
  }
340
+ /**
341
+ * A turn is in flight: the host has called `run()`/`stream()` and is awaiting it.
342
+ *
343
+ * ⚠️ HOSTS MUST BRACKET EVERY RUN, IN A `finally`. `TeamAgent` does not own the run call —
344
+ * the host holds the unwrapped `Agent` — so it cannot observe this for itself.
345
+ */
346
+ turnStartedAt = null;
347
+ /**
348
+ * How long a turn may be in flight before this stops believing the host.
349
+ *
350
+ * ⚠️ A FLAG THAT ONLY EVER GETS SET IS WORSE THAN THE BUG IT GUARDS. A host that throws past
351
+ * its `endTurn()` would block every edit to that agent for the life of the process, with no
352
+ * way back short of a restart. Going stale bounds that to one window; the edit that slips
353
+ * through after it is the same edit that would have been allowed before this guard existed.
354
+ */
355
+ static TURN_STALE_AFTER_MS = 10 * 60 * 1000;
356
+ /** Mark the start of a turn. Pair with `endTurn()` in a `finally`. */
357
+ beginTurn() {
358
+ this.turnStartedAt = Date.now();
359
+ }
360
+ /** Mark the end of a turn, however it ended — completed, aborted or thrown. */
361
+ endTurn() {
362
+ this.turnStartedAt = null;
363
+ }
364
+ /** Whether a turn is in flight and recent enough to still be believed. */
365
+ get isTurnInFlight() {
366
+ if (this.turnStartedAt === null)
367
+ return false;
368
+ return Date.now() - this.turnStartedAt < TeamAgent.TURN_STALE_AFTER_MS;
369
+ }
288
370
  /**
289
371
  * Apply an edit to this agent in place.
290
372
  *
@@ -308,6 +390,25 @@ export class TeamAgent {
308
390
  when it is not. Its `rebuilt` contribution is folded in below.
309
391
  */
310
392
  const tierChanged = patch.modelTier !== undefined && patch.modelTier !== this._modelTier;
393
+ /*
394
+ ⚠️ REFUSED MID-TURN, BEFORE ANYTHING IS MUTATED. A rebuild snapshots the history with
395
+ `serialize()` and drops the instance — but the in-flight `run()` holds its own reference
396
+ and finishes anyway, appending to an agent nobody will read again. Measured: the user
397
+ watches a reply stream in, and the rebuilt agent is handed the snapshot from BEFORE it,
398
+ with no memory of having said it.
399
+
400
+ Not a broken conversation — `setHistory` repairs tool pairing, so nothing 400s. It is
401
+ worse than that: the agent silently forgets a turn the user just watched, and the next
402
+ thing it says is built on a history the user can see is wrong.
403
+
404
+ Only rebuilding edits are refused. A rename or a new face is read from the roster at
405
+ render time and cannot tear anything, so those still apply mid-turn.
406
+ */
407
+ if ((needsRebuild || tierChanged) && this.isTurnInFlight) {
408
+ throw new Error(`Agent '${this.id}' is mid-reply. Changing its prompt, model, tools or skills rebuilds ` +
409
+ `it and would drop the turn it is in the middle of — wait for the reply to finish, ` +
410
+ `or stop it, then save.`);
411
+ }
311
412
  if (patch.modelTier !== undefined)
312
413
  this.setModelTier(patch.modelTier);
313
414
  if (patch.displayName !== undefined)
@@ -670,6 +771,9 @@ export class TeamAgent {
670
771
  personality = personality ?? legacy.personality;
671
772
  }
672
773
  }
774
+ else if (data.role !== 'custom' && systemPromptAddition) {
775
+ systemPromptAddition = splitLegacyRolePrompt(systemPromptAddition, data.role);
776
+ }
673
777
  const agent = new TeamAgent({
674
778
  id: data.id,
675
779
  displayName: data.displayName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",