@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.
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +28 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +9 -4
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +13 -6
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +4 -2
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/framework.d.ts +19 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +114 -37
- package/dist/src/framework.js.map +1 -1
- package/dist/src/types/agent.d.ts +12 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +37 -0
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/assistant-persist-refusal.test.d.ts +2 -0
- package/dist/test/assistant-persist-refusal.test.d.ts.map +1 -0
- package/dist/test/assistant-persist-refusal.test.js +63 -0
- package/dist/test/assistant-persist-refusal.test.js.map +1 -0
- package/dist/test/code-execution.test.js +17 -6
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/agent.ts +31 -1
- package/src/code-execution/py-runner.ts +12 -6
- package/src/code-execution/tool-definition.ts +4 -2
- package/src/framework.ts +124 -38
- package/src/types/agent.ts +12 -0
package/dist/src/framework.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
:
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
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
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
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).
|