@animalabs/agent-framework 0.7.2 → 0.7.4

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.
Files changed (31) hide show
  1. package/dist/src/agent.d.ts.map +1 -1
  2. package/dist/src/agent.js +28 -1
  3. package/dist/src/agent.js.map +1 -1
  4. package/dist/src/code-execution/py-runner.d.ts +9 -4
  5. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  6. package/dist/src/code-execution/py-runner.js +13 -6
  7. package/dist/src/code-execution/py-runner.js.map +1 -1
  8. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  9. package/dist/src/code-execution/tool-definition.js +4 -2
  10. package/dist/src/code-execution/tool-definition.js.map +1 -1
  11. package/dist/src/framework.d.ts +19 -0
  12. package/dist/src/framework.d.ts.map +1 -1
  13. package/dist/src/framework.js +114 -37
  14. package/dist/src/framework.js.map +1 -1
  15. package/dist/src/types/agent.d.ts +12 -0
  16. package/dist/src/types/agent.d.ts.map +1 -1
  17. package/dist/test/agent-runtime-settings.test.js +37 -0
  18. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  19. package/dist/test/assistant-persist-refusal.test.d.ts +2 -0
  20. package/dist/test/assistant-persist-refusal.test.d.ts.map +1 -0
  21. package/dist/test/assistant-persist-refusal.test.js +63 -0
  22. package/dist/test/assistant-persist-refusal.test.js.map +1 -0
  23. package/dist/test/code-execution.test.js +17 -6
  24. package/dist/test/code-execution.test.js.map +1 -1
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +1 -1
  27. package/src/agent.ts +31 -1
  28. package/src/code-execution/py-runner.ts +12 -6
  29. package/src/code-execution/tool-definition.ts +4 -2
  30. package/src/framework.ts +124 -38
  31. package/src/types/agent.ts +12 -0
@@ -1787,6 +1787,11 @@ export class AgentFramework {
1787
1787
  context_budget_tokens: { type: 'number', description: 'Total input context budget, including the reserved response allowance.' },
1788
1788
  tail_tokens: { type: 'number', description: 'Recent raw context retained verbatim.' },
1789
1789
  transition_pace_tokens: { type: 'number', description: 'Maximum ordinary KV re-read/perturbation per compile while converging.' },
1790
+ immediate: {
1791
+ type: 'boolean',
1792
+ description: 'With a context_budget_tokens DECREASE: apply it immediately instead of a paced descent — ' +
1793
+ 'the whole fold-down and its KV re-read land on the next compile, and any in-flight descent is cancelled.',
1794
+ },
1790
1795
  same_round_think_text_policy: {
1791
1796
  type: 'string',
1792
1797
  enum: ['public', 'private'],
@@ -3105,7 +3110,26 @@ export class AgentFramework {
3105
3110
  await this.routeConversationIncoming(event, metadata);
3106
3111
  return;
3107
3112
  }
3108
- const id = this.addMessage('user', event.content, metadata);
3113
+ // Addressed-while-closed invitation — parity with the push-event path
3114
+ // (channels.publish surfaces like portal-mcpl deliver mentions here).
3115
+ const incomingContent = [...event.content];
3116
+ {
3117
+ const invitation = this.buildClosedChannelInvitation({
3118
+ serverId: event.serverId,
3119
+ channelId: event.channelId,
3120
+ messageId: event.messageId,
3121
+ authorName: event.author?.name || 'Someone',
3122
+ guildName: typeof event.metadata?.guildName === 'string' && event.metadata.guildName
3123
+ ? event.metadata.guildName
3124
+ : undefined,
3125
+ tags: event.tags,
3126
+ });
3127
+ if (invitation) {
3128
+ incomingContent.push(invitation.block);
3129
+ Object.assign(metadata, invitation.metadataPatch);
3130
+ }
3131
+ }
3132
+ const id = this.addMessage('user', incomingContent, metadata);
3109
3133
  this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
3110
3134
  if (event.triggerInference) {
3111
3135
  const addressed = isAddressedMessage(event.tags, event.metadata);
@@ -3352,6 +3376,60 @@ export class AgentFramework {
3352
3376
  }
3353
3377
  }
3354
3378
  }
3379
+ /**
3380
+ * Build the addressed-while-CLOSED channel invitation block, shared by the
3381
+ * push-event and channels/incoming ingest paths. Until 2026-07-27 only push
3382
+ * events (discord-mcpl mentions) carried it; channels.publish surfaces
3383
+ * (portal-mcpl) delivered bare mentions with no guidance and no route — an
3384
+ * explicit-prose agent's natural plain reply bounced with nowhere to go
3385
+ * (Rhys's first portal mention: a 2,320-char reply stranded).
3386
+ *
3387
+ * Wording: option 1 teaches the `>>` destination prefix, which delivers for
3388
+ * BOTH legacy (host-inferred) and explicit prose-routing agents. The
3389
+ * previous "simply write your reply as normal text" was a false promise
3390
+ * under explicit routing (the metadata flags it set had no consumers).
3391
+ *
3392
+ * Human-readable names wherever the surface provided them — a mind reading
3393
+ * this should see WHO addressed it WHERE, not a wall of snowflake ids. Raw
3394
+ * ids remain only inside the tool-argument instructions, where they are the
3395
+ * literal values to pass.
3396
+ */
3397
+ buildClosedChannelInvitation(opts) {
3398
+ if (!this.channelRegistry)
3399
+ return null;
3400
+ if (!opts.tags?.includes('chat:addressed'))
3401
+ return null;
3402
+ if (this.channelRegistry.isChannelOpen(opts.channelId))
3403
+ return null;
3404
+ const descriptor = this.channelRegistry.getDescriptor(opts.channelId);
3405
+ const maxBackscroll = descriptor?.capabilities?.history?.maxMessages ?? 0;
3406
+ const label = opts.channelLabel ?? descriptor?.label;
3407
+ const channelLabel = label ? `#${label}` : `"${opts.channelId}"`;
3408
+ const place = opts.guildName ? `${channelLabel} in "${opts.guildName}"` : channelLabel;
3409
+ const block = {
3410
+ type: 'text',
3411
+ text: `\n[Channel invitation] ${opts.authorName} addressed you in ${place} — ` +
3412
+ `a channel you haven't joined. You're seeing this one message; you won't receive more from it unless you join. ` +
3413
+ `Your options:\n` +
3414
+ `1. Reply without joining — write your reply this turn prefixed with ">>${channelLabel}"; it will be delivered there.\n` +
3415
+ `2. Join the channel — call channel_open with channelId "${opts.channelId}" and serverId "${opts.serverId}"` +
3416
+ (maxBackscroll > 0
3417
+ ? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId "${opts.messageId}".\n`
3418
+ : `.\n`) +
3419
+ `3. Stay out — call channel_decline with channelId "${opts.channelId}", serverId "${opts.serverId}", ` +
3420
+ `and messageId "${opts.messageId}" (optionally set acknowledge to an emoji like 👀 so ${opts.authorName} isn't left hanging).\n` +
3421
+ `Doing nothing is also fine.`,
3422
+ };
3423
+ return {
3424
+ block,
3425
+ metadataPatch: {
3426
+ channelInvitation: true,
3427
+ channelOpen: false,
3428
+ channelId: opts.channelId,
3429
+ invitationMessageId: opts.messageId,
3430
+ },
3431
+ };
3432
+ }
3355
3433
  /**
3356
3434
  * Convert an MCPL push event to a context message.
3357
3435
  */
@@ -3378,44 +3456,21 @@ export class AgentFramework {
3378
3456
  metadata.channelId = triggerChannel.channelId;
3379
3457
  }
3380
3458
  const content = [...event.content];
3381
- const addressedWhileClosed = triggerChannel &&
3382
- event.tags?.includes('chat:addressed') &&
3383
- this.channelRegistry &&
3384
- !this.channelRegistry.isChannelOpen(triggerChannel.channelId);
3385
- if (addressedWhileClosed) {
3386
- const descriptor = this.channelRegistry.getDescriptor(triggerChannel.channelId);
3387
- const messageId = typeof event.origin?.messageId === 'string'
3388
- ? event.origin.messageId
3389
- : event.eventId;
3390
- const maxBackscroll = descriptor?.capabilities?.history?.maxMessages ?? 0;
3391
- // Resolve human-readable names from the event origin wherever the
3392
- // surface provided them — a mind reading this should see WHO addressed
3393
- // it WHERE, not a wall of snowflake ids. Raw ids remain only inside the
3394
- // tool-argument instructions, where they are the literal values to pass.
3459
+ if (triggerChannel) {
3395
3460
  const origin = (event.origin ?? {});
3396
- const authorName = typeof origin.authorName === 'string' && origin.authorName ? origin.authorName : 'Someone';
3397
- const channelLabel = triggerChannel.label ? `#${triggerChannel.label}` : `"${triggerChannel.channelId}"`;
3398
- const place = typeof origin.guildName === 'string' && origin.guildName
3399
- ? `${channelLabel} in "${origin.guildName}"`
3400
- : channelLabel;
3401
- content.push({
3402
- type: 'text',
3403
- text: `\n[Channel invitation] ${authorName} addressed you in ${place} — ` +
3404
- `a channel you haven't joined. You're seeing this one message; you won't receive more from it unless you join. ` +
3405
- `Your options:\n` +
3406
- `1. Reply without joining — simply write your reply as normal text this turn; it will be delivered to ${channelLabel}.\n` +
3407
- `2. Join the channel — call channel_open with channelId "${triggerChannel.channelId}" and serverId "${event.serverId}"` +
3408
- (maxBackscroll > 0
3409
- ? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId "${messageId}".\n`
3410
- : `.\n`) +
3411
- `3. Stay out — call channel_decline with channelId "${triggerChannel.channelId}", serverId "${event.serverId}", ` +
3412
- `and messageId "${messageId}" (optionally set acknowledge to an emoji like 👀 so ${authorName} isn't left hanging).\n` +
3413
- `Doing nothing is also fine.`,
3461
+ const invitation = this.buildClosedChannelInvitation({
3462
+ serverId: event.serverId,
3463
+ channelId: triggerChannel.channelId,
3464
+ channelLabel: triggerChannel.label,
3465
+ messageId: typeof origin.messageId === 'string' ? origin.messageId : event.eventId,
3466
+ authorName: typeof origin.authorName === 'string' && origin.authorName ? origin.authorName : 'Someone',
3467
+ guildName: typeof origin.guildName === 'string' && origin.guildName ? origin.guildName : undefined,
3468
+ tags: event.tags,
3414
3469
  });
3415
- metadata.channelInvitation = true;
3416
- metadata.channelOpen = false;
3417
- metadata.channelId = triggerChannel.channelId;
3418
- metadata.invitationMessageId = messageId;
3470
+ if (invitation) {
3471
+ content.push(invitation.block);
3472
+ Object.assign(metadata, invitation.metadataPatch);
3473
+ }
3419
3474
  }
3420
3475
  const id = this.addMessage('user', content, metadata);
3421
3476
  this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:push-event' });
@@ -3913,6 +3968,20 @@ export class AgentFramework {
3913
3968
  });
3914
3969
  this.eventGate?.onInferenceStarted(agent.name);
3915
3970
  this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), startedAt: Date.now() });
3971
+ // Typing indicator, started at TURN START. It says "attending", which is
3972
+ // true from the moment the turn is accepted — but it used to start inside
3973
+ // driveStream, i.e. only after module gatherContext + MCPL beforeInference
3974
+ // RPCs + the full context compile + stream initiation. All of that read as
3975
+ // dead air to whoever just messaged the agent (30+ s during the 2026-07
3976
+ // mythos compile regression; still seconds of hooks+compile after).
3977
+ // Same channel choice as driveStream's own startTyping (which stays, is
3978
+ // idempotent per channel, and owns the 7s refresh); the catch below stops
3979
+ // it on the no-driveStream failure paths (e.g. a compile refusal).
3980
+ const earlyTypingChannel = agent.proseRouting === 'explicit'
3981
+ ? trigger?.channelId ?? null
3982
+ : this.turnLocusPins.get(agent.name) ?? null;
3983
+ if (earlyTypingChannel)
3984
+ this.channelRegistry?.startTyping(earlyTypingChannel);
3916
3985
  try {
3917
3986
  const requestSnapshot = this.captureInferenceToolSnapshot(agent);
3918
3987
  const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
@@ -3956,6 +4025,10 @@ export class AgentFramework {
3956
4025
  this.activeStreams.set(agent.name, handle);
3957
4026
  }
3958
4027
  catch (error) {
4028
+ // The early typing indicator has no driveStream `finally` on this path —
4029
+ // a hook/compile/stream-setup failure must not leave "typing…" stuck.
4030
+ // (Retries below restart it; stopTyping is idempotent.)
4031
+ this.channelRegistry?.stopTyping();
3959
4032
  const err = error instanceof Error ? error : new Error(String(error));
3960
4033
  this.emitTrace({
3961
4034
  type: 'inference:failed',
@@ -7243,6 +7316,10 @@ export class AgentFramework {
7243
7316
  if (input.same_round_think_text_policy !== undefined) {
7244
7317
  patch.sameRoundThinkTextPolicy = input.same_round_think_text_policy;
7245
7318
  }
7319
+ // Immediate budget decrease: skip the paced descent; the next
7320
+ // compile plans straight at the new budget (one-shot fold-down).
7321
+ if (input.immediate !== undefined)
7322
+ patch.immediate = Boolean(input.immediate);
7246
7323
  // Route extension-owned keys to their modules; apply the core patch
7247
7324
  // only when it touches core keys (an extension-only update must not
7248
7325
  // disturb a converging budget transition).