@animalabs/agent-framework 0.18.0 → 0.19.0
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/framework.d.ts +8 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +14 -11
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +15 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +50 -4
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/test/channels-changed-respond-first.test.d.ts +2 -0
- package/dist/test/channels-changed-respond-first.test.d.ts.map +1 -0
- package/dist/test/channels-changed-respond-first.test.js +74 -0
- package/dist/test/channels-changed-respond-first.test.js.map +1 -0
- package/dist/test/journal-tool.test.d.ts +2 -0
- package/dist/test/journal-tool.test.d.ts.map +1 -0
- package/dist/test/journal-tool.test.js +33 -0
- package/dist/test/journal-tool.test.js.map +1 -0
- package/dist/test/overbudget-drain-breaker.test.js +44 -4
- package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/framework.ts +14 -11
- package/src/mcpl/channel-registry.ts +55 -3
package/package.json
CHANGED
package/src/framework.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeF
|
|
|
5
5
|
import { JsStore } from '@animalabs/chronicle';
|
|
6
6
|
import type { Membrane, ContentBlock, NormalizedRequest, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
|
|
7
7
|
import { MembraneError } from '@animalabs/membrane';
|
|
8
|
-
import { ContextManager, PassthroughStrategy, WindowedPassthroughStrategy } from '@animalabs/context-manager';
|
|
8
|
+
import { ContextManager, PassthroughStrategy, WindowedPassthroughStrategy, OverBudgetError, UncoveredDropError } from '@animalabs/context-manager';
|
|
9
9
|
import type { CacheWireReceipt } from './kv-unified-wire.js';
|
|
10
10
|
import { SUBCONSCIOUS_TOOLS, SUBCONSCIOUS_TOOL_NAMES, type SubconsciousConfig } from './tune-out/tools.js';
|
|
11
11
|
import { TuneOutCoordinator, TUNE_OUT_DEFAULTS } from './tune-out/coordinator.js';
|
|
@@ -11030,11 +11030,11 @@ export class AgentFramework {
|
|
|
11030
11030
|
return;
|
|
11031
11031
|
}
|
|
11032
11032
|
|
|
11033
|
-
// Route synthesized 'think' (private reasoning)
|
|
11034
|
-
// stay-silent) tools — handled by the channel registry like the other
|
|
11033
|
+
// Route synthesized 'think' (private reasoning), 'journal' (private
|
|
11034
|
+
// long-form notes) and 'skip_reply' (deliberate stay-silent) tools — handled by the channel registry like the other
|
|
11035
11035
|
// synthesized channel tools, but they aren't `channel_`-prefixed so they
|
|
11036
11036
|
// need an explicit route here.
|
|
11037
|
-
if ((enrichedCall.name === 'think' || enrichedCall.name === 'skip_reply') && this.channelRegistry) {
|
|
11037
|
+
if ((enrichedCall.name === 'think' || enrichedCall.name === 'journal' || enrichedCall.name === 'skip_reply') && this.channelRegistry) {
|
|
11038
11038
|
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
11039
11039
|
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
11040
11040
|
// the agent after N seconds unless something else wakes it first
|
|
@@ -11557,24 +11557,27 @@ export class AgentFramework {
|
|
|
11557
11557
|
* quarantine, OverBudget drain kick) gate on `errorType`, so classification
|
|
11558
11558
|
* drift between sites would silently disable a safety net.
|
|
11559
11559
|
*
|
|
11560
|
-
* context-manager's
|
|
11561
|
-
*
|
|
11562
|
-
*
|
|
11563
|
-
*
|
|
11564
|
-
*
|
|
11560
|
+
* context-manager's refusal errors are matched by a real `instanceof` since
|
|
11561
|
+
* CM exports them from its package root (context-manager#41 / #71; floor
|
|
11562
|
+
* `^0.7.0`, ours is higher). The `err.name` comparison stays as a FALLBACK
|
|
11563
|
+
* only: a deployment that ends up with two CM copies (a peer package
|
|
11564
|
+
* pinning a different version) constructs errors from a different class
|
|
11565
|
+
* object, and `name` — set in the constructor — survives that boundary.
|
|
11566
|
+
* Deliberately NOT a message match: the message wording belongs to CM and
|
|
11567
|
+
* can be reworded without warning.
|
|
11565
11568
|
*/
|
|
11566
11569
|
private classifyInferenceError(err: Error): { retryable?: boolean; errorType?: string } {
|
|
11567
11570
|
if (err instanceof MembraneError) {
|
|
11568
11571
|
return { retryable: err.retryable, errorType: err.type };
|
|
11569
11572
|
}
|
|
11570
|
-
if (err.name === 'OverBudgetError') {
|
|
11573
|
+
if (err instanceof OverBudgetError || err.name === 'OverBudgetError') {
|
|
11571
11574
|
return { errorType: 'over_budget' };
|
|
11572
11575
|
}
|
|
11573
11576
|
// context-manager's fatal coverage invariant (fix/coverage-invariant-fatal,
|
|
11574
11577
|
// 2026-07-26): a compile REFUSES rather than shipping a context with
|
|
11575
11578
|
// silently-dropped messages. Recovery is identical to over_budget — kick
|
|
11576
11579
|
// the compression drain so summaries cover the un-represented span.
|
|
11577
|
-
if (err.name === 'UncoveredDropError') {
|
|
11580
|
+
if (err instanceof UncoveredDropError || err.name === 'UncoveredDropError') {
|
|
11578
11581
|
return { errorType: 'context_refusal' };
|
|
11579
11582
|
}
|
|
11580
11583
|
return {};
|
|
@@ -401,6 +401,22 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
401
401
|
required: [],
|
|
402
402
|
},
|
|
403
403
|
},
|
|
404
|
+
{
|
|
405
|
+
name: 'journal',
|
|
406
|
+
description:
|
|
407
|
+
'Write an entry in your private journal. The entry stays in your own context and ' +
|
|
408
|
+
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
409
|
+
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
410
|
+
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
411
|
+
'text is routed; to end the turn without replying, call skip_reply afterwards.',
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: 'object' as const,
|
|
414
|
+
properties: {
|
|
415
|
+
content: { type: 'string', description: 'The journal entry (private; not sent anywhere).' },
|
|
416
|
+
},
|
|
417
|
+
required: ['content'],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
404
420
|
{
|
|
405
421
|
name: 'skip_reply',
|
|
406
422
|
description:
|
|
@@ -414,7 +430,9 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
414
430
|
properties: {
|
|
415
431
|
reason: {
|
|
416
432
|
type: 'string',
|
|
417
|
-
description:
|
|
433
|
+
description:
|
|
434
|
+
'Optional ONE short line on why you are not replying (private; not sent anywhere). ' +
|
|
435
|
+
'Keep it under ~100 characters — put anything longer in journal() first.',
|
|
418
436
|
},
|
|
419
437
|
wake_in_seconds: {
|
|
420
438
|
type: 'number',
|
|
@@ -773,7 +791,8 @@ export class ChannelRegistry {
|
|
|
773
791
|
}
|
|
774
792
|
}
|
|
775
793
|
|
|
776
|
-
// Process added channels (validate per-descriptor, register
|
|
794
|
+
// Process added channels (validate per-descriptor, register; reconcile
|
|
795
|
+
// after responding, below)
|
|
777
796
|
const accepted: typeof params.added = [];
|
|
778
797
|
if (params.added) {
|
|
779
798
|
for (const channel of params.added) {
|
|
@@ -792,10 +811,18 @@ export class ChannelRegistry {
|
|
|
792
811
|
addedResults.push({ id: channel.id, accepted: true });
|
|
793
812
|
accepted.push(channel);
|
|
794
813
|
}
|
|
795
|
-
if (accepted.length > 0) await this.reconcileChannels(serverId, accepted);
|
|
796
814
|
}
|
|
815
|
+
|
|
816
|
+
// Respond before reconciliation, as handleRegister does. A server that
|
|
817
|
+
// announces from inside a request it is serving (a tool that refreshes
|
|
818
|
+
// or subscribes) cannot read the channels/open or channels/close that
|
|
819
|
+
// reconciling sends until this response arrives; reconciling first
|
|
820
|
+
// deadlocks both sides until one times out (#160). The verdicts are
|
|
821
|
+
// settled above, so nothing in the response depends on reconciling.
|
|
797
822
|
responder?.respond({ results: addedResults });
|
|
798
823
|
|
|
824
|
+
if (accepted.length > 0) await this.reconcileChannels(serverId, accepted);
|
|
825
|
+
|
|
799
826
|
this.emitTraceFn({
|
|
800
827
|
type: 'mcpl:channels-changed',
|
|
801
828
|
serverId,
|
|
@@ -1218,6 +1245,9 @@ export class ChannelRegistry {
|
|
|
1218
1245
|
case 'think':
|
|
1219
1246
|
return this.handleToolThink(input as { content?: string });
|
|
1220
1247
|
|
|
1248
|
+
case 'journal':
|
|
1249
|
+
return this.handleToolJournal(input as { content?: string });
|
|
1250
|
+
|
|
1221
1251
|
case 'skip_reply':
|
|
1222
1252
|
return this.handleToolSkipReply(input as { reason?: string; wake_in_seconds?: number });
|
|
1223
1253
|
|
|
@@ -2670,6 +2700,28 @@ export class ChannelRegistry {
|
|
|
2670
2700
|
};
|
|
2671
2701
|
}
|
|
2672
2702
|
|
|
2703
|
+
/**
|
|
2704
|
+
* Handle the synthesized `journal` tool — a private place for long-form
|
|
2705
|
+
* notes. Sends nothing, does not end the turn, does not touch prose routing.
|
|
2706
|
+
*
|
|
2707
|
+
* Why it exists (sill, 2026-09-19): residents were keeping 2–3KB diaries in
|
|
2708
|
+
* `skip_reply.reason`. Long prose in a private-REASONING tool argument
|
|
2709
|
+
* (`skip_reply.reason`, `think.content`) makes replayed history read as a
|
|
2710
|
+
* reasoning trace, and every memory-compression request over it is refused
|
|
2711
|
+
* `reasoning_extraction` regardless of content; the same prose in a
|
|
2712
|
+
* note-taking tool passes (canary record: context-manager
|
|
2713
|
+
* `tool-prose-hoist.ts`, whose fallback rung rewrites old history into calls
|
|
2714
|
+
* to THIS tool — so the result wording below is mirrored there as
|
|
2715
|
+
* DEFAULT_TOOL_PROSE_RESULT; keep the two in step).
|
|
2716
|
+
*/
|
|
2717
|
+
private handleToolJournal(_input: { content?: string }): ToolResult {
|
|
2718
|
+
return {
|
|
2719
|
+
success: true,
|
|
2720
|
+
// No echo: the entry is already in the tool_use block.
|
|
2721
|
+
data: { recorded: true, note: 'Journal entry recorded (private — not sent anywhere).' },
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2673
2725
|
/**
|
|
2674
2726
|
* Handle the synthesized `skip_reply` tool — the deliberate "stay silent"
|
|
2675
2727
|
* signal. A no-op as far as any surface is concerned (sends nothing); its
|