@animalabs/agent-framework 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.
@@ -4,4 +4,16 @@ import type { ContentBlock } from '@animalabs/membrane';
4
4
  * set, every other block is the same object.
5
5
  */
6
6
  export declare function stampThinkingTokenEstimates(blocks: ContentBlock[], outputTokens: number): ContentBlock[];
7
+ /**
8
+ * A request's messages with no top-level block carrying `tokenEstimate`. Both
9
+ * of an agent's request builders call it on the compiled messages: a
10
+ * framework turn's (`Agent.assembleActivationRequest`) and the direct API's
11
+ * (`Agent.runInferenceWithInjections`). A block that carries the stamp is
12
+ * copied without it, and so is the message that holds it. Every other
13
+ * message and block is passed on as it is, and nothing is changed in place,
14
+ * so the stored block keeps its stamp.
15
+ */
16
+ export declare function withoutTokenEstimates<T extends {
17
+ content: ContentBlock[];
18
+ }>(messages: T[]): T[];
7
19
  //# sourceMappingURL=thinking-token-stamp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"thinking-token-stamp.d.ts","sourceRoot":"","sources":["../../src/thinking-token-stamp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAiExD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,YAAY,EAAE,CAyBxG"}
1
+ {"version":3,"file":"thinking-token-stamp.d.ts","sourceRoot":"","sources":["../../src/thinking-token-stamp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAqExD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,YAAY,EAAE,CAyBxG;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,YAAY,EAAE,CAAA;CAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAY/F"}
@@ -24,9 +24,13 @@
24
24
  * the hidden thinking is the bulk of the output, and this stamp is what makes
25
25
  * the compiled budget honest (see context-manager `SIGNATURE_CHARS_PER_TOKEN`).
26
26
  *
27
- * Wire safety: membrane's Anthropic formatter builds thinking blocks field
28
- * by field (`type`, `thinking`, `signature`), so the stamp never reaches the
29
- * provider; the signature itself is copied, never edited.
27
+ * Wire safety: the stamp is context-manager's pricing metadata, and no field
28
+ * a provider takes. The Messages API refuses a block that carries it
29
+ * ("Extra inputs are not permitted"), and membrane (0.5.87 and 0.6.0) sends a
30
+ * `redacted_thinking` block to Anthropic and Bedrock with every field it has.
31
+ * So an agent's request builders drop the stamp from the compiled messages
32
+ * (`withoutTokenEstimates`), and the stored block, which context-manager
33
+ * prices, keeps it. The signature itself is copied, never edited.
30
34
  */
31
35
  const PROSE_CHARS_PER_TOKEN = 2.9;
32
36
  const DENSE_CHARS_PER_TOKEN = 2.3;
@@ -100,4 +104,31 @@ export function stampThinkingTokenEstimates(blocks, outputTokens) {
100
104
  return { ...block, tokenEstimate };
101
105
  });
102
106
  }
107
+ /**
108
+ * A request's messages with no top-level block carrying `tokenEstimate`. Both
109
+ * of an agent's request builders call it on the compiled messages: a
110
+ * framework turn's (`Agent.assembleActivationRequest`) and the direct API's
111
+ * (`Agent.runInferenceWithInjections`). A block that carries the stamp is
112
+ * copied without it, and so is the message that holds it. Every other
113
+ * message and block is passed on as it is, and nothing is changed in place,
114
+ * so the stored block keeps its stamp.
115
+ */
116
+ export function withoutTokenEstimates(messages) {
117
+ return messages.map((message) => {
118
+ if (!message.content.some(isStamped))
119
+ return message;
120
+ return {
121
+ ...message,
122
+ content: message.content.map((block) => {
123
+ if (!isStamped(block))
124
+ return block;
125
+ const { tokenEstimate: _stamp, ...unstamped } = block;
126
+ return unstamped;
127
+ }),
128
+ };
129
+ });
130
+ }
131
+ function isStamped(block) {
132
+ return typeof block === 'object' && block !== null && 'tokenEstimate' in block;
133
+ }
103
134
  //# sourceMappingURL=thinking-token-stamp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"thinking-token-stamp.js","sourceRoot":"","sources":["../../src/thinking-token-stamp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thinking-token-stamp.js","sourceRoot":"","sources":["../../src/thinking-token-stamp.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
package/src/agent.ts CHANGED
@@ -3,6 +3,7 @@ import { isAbortedResponse } from '@animalabs/membrane';
3
3
  import { createHash } from 'node:crypto';
4
4
  import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
5
5
  import { ToolResultGuard } from './tool-result-guard.js';
6
+ import { withoutTokenEstimates } from './thinking-token-stamp.js';
6
7
  import { assertBooleanSetting, holdsSettings } from './config-checks.js';
7
8
  import {
8
9
  CONTINUE_ROW,
@@ -1101,8 +1102,11 @@ export class Agent {
1101
1102
  // notice.
1102
1103
  let unguardedResults: ReturnType<Agent['buildToolResultMessages']> | undefined;
1103
1104
  const assemble = async (): Promise<NormalizedRequest> => {
1104
- const { messages, systemInjections } = await this.toolResultGuard.afterAnnotations(
1105
+ const compiled = await this.toolResultGuard.afterAnnotations(
1105
1106
  () => this.compileWithInjections(budget, injections));
1107
+ const { systemInjections } = compiled;
1108
+ // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1109
+ const messages = withoutTokenEstimates(compiled.messages);
1106
1110
  // Read once, after the first compile, since results may have become
1107
1111
  // ready while it ran; the refusal retry, assembled while the agent is
1108
1112
  // inferring, sends the same ones.
@@ -1298,6 +1302,8 @@ export class Agent {
1298
1302
  ),
1299
1303
  }))
1300
1304
  .filter((m) => m.content.length > 0);
1305
+ // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1306
+ messages = withoutTokenEstimates(messages);
1301
1307
 
1302
1308
  // Silent heartbeat ticks and replies to a [Continue] turn store no
1303
1309
  // prompt row. Render each request-only turn before its response's first
package/src/framework.ts CHANGED
@@ -282,6 +282,28 @@ const silenceCauseOf = (calls: Array<{ name: string; input?: unknown }>): 'send'
282
282
  return calls.some(isSilencingCall) ? 'send' : null;
283
283
  };
284
284
 
285
+ /**
286
+ * The keys of a stored message's metadata that only the host writes and that
287
+ * some reader takes on its own as what the message is: `system` and `kind`
288
+ * make it a host notice of that kind, `draftOwner` and `draftIds` one that
289
+ * names those drafts, `tuneOut` a message a tune-out holds from the
290
+ * residents, `coalescingSubject` (beside the occurrence's `eventId`) the
291
+ * delivery of a coalesced occurrence, `deferredWriteId` the landed copy of a
292
+ * deferred write, `external` a module's own row for an outside message, and
293
+ * `openaiResponsesItems` membrane's mark of an imported Responses history. A
294
+ * server's metadata, a channel message's `metadata` or a push event's
295
+ * `origin`, is stored without them, and the host writes its own after it:
296
+ * MCPL SPEC §16.6 makes metadata the producer's untrusted claims, which a
297
+ * host may disbelieve. Keys a server writes by protocol, such as `tags` and
298
+ * `mentioned`, stay. So do the keys a reader takes only beside one of these:
299
+ * a failed reply's `reason` and `detail` beside its `kind`, and the Discord
300
+ * module's `discordTextFirst` on a row it found by `external`.
301
+ */
302
+ const HOST_METADATA_KEYS = [
303
+ 'system', 'kind', 'draftOwner', 'draftIds', 'tuneOut', 'coalescingSubject', 'deferredWriteId',
304
+ 'external', 'openaiResponsesItems',
305
+ ] as const;
306
+
285
307
  /**
286
308
  * True when an injected message is real conversational input — something the
287
309
  * agent might actually be replying to — rather than ambient machinery.
@@ -10852,9 +10874,9 @@ export class AgentFramework {
10852
10874
  // turn ends. The window stores them here, right after the
10853
10875
  // tool_result message, which is exactly where the membrane appends
10854
10876
  // them in the live conversation — same participant, same content
10855
- // blocks, same metadata, and compile() emits recent messages
10856
- // verbatim, so the next turn's compiled prefix byte-matches the
10857
- // live request (prompt-cache safe).
10877
+ // blocks, and neither carries the row's metadata — and compile()
10878
+ // emits recent messages verbatim, so the next turn's compiled prefix
10879
+ // byte-matches the live request (prompt-cache safe).
10858
10880
  //
10859
10881
  // The queue is drained ONLY at the target agent's own boundary:
10860
10882
  // draining at another agent's boundary would store the messages
@@ -11109,10 +11131,14 @@ export class AgentFramework {
11109
11131
  // Streaming path: convert results and resume the stream.
11110
11132
  // Mid-turn messages collected above ride along as injected user
11111
11133
  // messages (membrane ≥0.5.72) — appended after the tool_result
11112
- // envelope so the next round of THIS turn hears them.
11134
+ // envelope so the next round of THIS turn hears them. Each goes
11135
+ // as its participant and content alone: a stored row's metadata
11136
+ // is the host's record, not input for membrane.
11113
11137
  currentState.stream.provideToolResults(
11114
11138
  agent.toolResultGuard.submissionResults(membraneResults),
11115
- midTurnInjections.length > 0 ? { injectedMessages: midTurnInjections } : undefined,
11139
+ midTurnInjections.length > 0
11140
+ ? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
11141
+ : undefined,
11116
11142
  );
11117
11143
  // RFC-006 §3.3: consumption is determined at assembly, and this
11118
11144
  // is assembly too when the stream carries the injected messages
@@ -11313,6 +11339,31 @@ export class AgentFramework {
11313
11339
  // Built-in MCPL event → context message conversion
11314
11340
  // ==========================================================================
11315
11341
 
11342
+ /** The servers and keys whose drop withoutHostKeys has logged, each as
11343
+ * `${serverId}\u0000${key}`. */
11344
+ private readonly hostKeysDropped = new Set<string>();
11345
+
11346
+ /**
11347
+ * A server's metadata as it is stored: a copy without HOST_METADATA_KEYS.
11348
+ * A server may use one of those names for something of its own, so the
11349
+ * first drop of each key from each server is logged.
11350
+ */
11351
+ private withoutHostKeys(serverId: string, given: Record<string, unknown> | undefined): Record<string, unknown> {
11352
+ const kept: Record<string, unknown> = { ...given };
11353
+ for (const key of HOST_METADATA_KEYS) {
11354
+ if (!Object.hasOwn(kept, key)) continue;
11355
+ delete kept[key];
11356
+ const logged = `${serverId}\u0000${key}`;
11357
+ if (this.hostKeysDropped.has(logged)) continue;
11358
+ this.hostKeysDropped.add(logged);
11359
+ console.error(
11360
+ `[mcpl] ${serverId}: dropped "${key}" from a message, since only the host writes that key ` +
11361
+ `(logged once for each server and key)`,
11362
+ );
11363
+ }
11364
+ return kept;
11365
+ }
11366
+
11316
11367
  /**
11317
11368
  * Convert an incoming MCPL channel message to a context message.
11318
11369
  * This replaces the old MCPLModule.onProcess() message conversion.
@@ -11349,7 +11400,7 @@ export class AgentFramework {
11349
11400
  return undefined;
11350
11401
  }
11351
11402
  const metadata: Record<string, unknown> = {
11352
- ...event.metadata,
11403
+ ...this.withoutHostKeys(event.serverId, event.metadata),
11353
11404
  ...(event.eventId ? { eventId: event.eventId } : {}),
11354
11405
  ...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
11355
11406
  channelId: event.channelId,
@@ -12810,7 +12861,7 @@ export class AgentFramework {
12810
12861
  }
12811
12862
 
12812
12863
  const metadata: Record<string, unknown> = {
12813
- ...event.origin,
12864
+ ...this.withoutHostKeys(event.serverId, event.origin),
12814
12865
  serverId: event.serverId,
12815
12866
  featureSet: event.featureSet,
12816
12867
  eventId: event.eventId,
@@ -26,9 +26,13 @@ import type { ContentBlock } from '@animalabs/membrane';
26
26
  * the hidden thinking is the bulk of the output, and this stamp is what makes
27
27
  * the compiled budget honest (see context-manager `SIGNATURE_CHARS_PER_TOKEN`).
28
28
  *
29
- * Wire safety: membrane's Anthropic formatter builds thinking blocks field
30
- * by field (`type`, `thinking`, `signature`), so the stamp never reaches the
31
- * provider; the signature itself is copied, never edited.
29
+ * Wire safety: the stamp is context-manager's pricing metadata, and no field
30
+ * a provider takes. The Messages API refuses a block that carries it
31
+ * ("Extra inputs are not permitted"), and membrane (0.5.87 and 0.6.0) sends a
32
+ * `redacted_thinking` block to Anthropic and Bedrock with every field it has.
33
+ * So an agent's request builders drop the stamp from the compiled messages
34
+ * (`withoutTokenEstimates`), and the stored block, which context-manager
35
+ * prices, keeps it. The signature itself is copied, never edited.
32
36
  */
33
37
  const PROSE_CHARS_PER_TOKEN = 2.9;
34
38
  const DENSE_CHARS_PER_TOKEN = 2.3;
@@ -93,3 +97,30 @@ export function stampThinkingTokenEstimates(blocks: ContentBlock[], outputTokens
93
97
  return { ...block, tokenEstimate } as ContentBlock;
94
98
  });
95
99
  }
100
+
101
+ /**
102
+ * A request's messages with no top-level block carrying `tokenEstimate`. Both
103
+ * of an agent's request builders call it on the compiled messages: a
104
+ * framework turn's (`Agent.assembleActivationRequest`) and the direct API's
105
+ * (`Agent.runInferenceWithInjections`). A block that carries the stamp is
106
+ * copied without it, and so is the message that holds it. Every other
107
+ * message and block is passed on as it is, and nothing is changed in place,
108
+ * so the stored block keeps its stamp.
109
+ */
110
+ export function withoutTokenEstimates<T extends { content: ContentBlock[] }>(messages: T[]): T[] {
111
+ return messages.map((message) => {
112
+ if (!message.content.some(isStamped)) return message;
113
+ return {
114
+ ...message,
115
+ content: message.content.map((block) => {
116
+ if (!isStamped(block)) return block;
117
+ const { tokenEstimate: _stamp, ...unstamped } = block as ContentBlock & { tokenEstimate?: unknown };
118
+ return unstamped as ContentBlock;
119
+ }),
120
+ };
121
+ });
122
+ }
123
+
124
+ function isStamped(block: ContentBlock): boolean {
125
+ return typeof block === 'object' && block !== null && 'tokenEstimate' in block;
126
+ }