Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ unread backlog is simply its unread backlog. Receipts and subject history are
kept in the `mcpl/coalescing` state (retry window 1 h); audit lines are
`mcpl:coalescing` trace events.

#### Per-channel conversation routing (deprecated)

> **Deprecated: `FrameworkConfig.conversations` (`ConversationRouter`).** Per-channel conversation routing sends each channel's traffic to its own fork agent, spawned from a template agent and closed after an idle TTL. It is being retired ([#235](https://github.com/anima-research/agent-framework/issues/235)). Its `'mention'` bind/trigger rule, the default for channels, reads `metadata.mentioned`, which discord-mcpl does not set, so on Discord channels an @-mention never binds a fork or triggers a bound one. Don't adopt it in new hosts. Existing configurations still route exactly as before, and the framework logs one `[deprecated]` line when it is created with `conversations` set. Removal is a follow-up.

### Streaming Lifecycle

1. **Start**: Framework calls `agent.startStreamWithInjections()` → `YieldingStream`
Expand Down
11 changes: 11 additions & 0 deletions changelog.d/deprecate-conversation-routing.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- **Per-channel conversation routing is deprecated** (`FrameworkConfig.conversations`,
`ConversationRouter`, `ConversationRouterConfig`). Its `'mention'` bind/trigger
rule, the default for channels, reads `metadata.mentioned`, which discord-mcpl
never sets, so on Discord channels an @-mention neither binds a fork nor
triggers a bound one (#235). Behavior is unchanged for now: routing still
works, and the framework logs one `[deprecated]` line when it is created with
`conversations` set. Removal is a follow-up. The `idleTtlMs` docs now note
that expiry is checked at most about once a minute, so the sweep usually
notices an expired binding up to ~60s after the TTL; that is not a deadline
for closing the fork, whose closure turn can be delayed further (e.g. while
the host is quiesced).
5 changes: 4 additions & 1 deletion src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { ProseStreamRouter } from './mcpl/prose-stream-router.js';
import { detectKnownToolWrapperProse } from './tool-wrapper-prose-guard.js';
import { InferenceRouter } from './mcpl/inference-router.js';
import { ChannelRegistry, type ChannelToolOrigin } from './mcpl/channel-registry.js';
import { ConversationRouter } from './mcpl/conversation-router.js';
import { ConversationRouter, CONVERSATION_ROUTING_DEPRECATION_NOTICE } from './mcpl/conversation-router.js';
import { safeSlice } from './safe-slice.js';
import type { WorkspaceModule } from './modules/workspace/index.js';
import {
Expand Down Expand Up @@ -1623,6 +1623,9 @@ export class AgentFramework {
);
}
framework.conversationRouter = new ConversationRouter(config.conversations);
// Deprecated feature (agent-framework#235): still wired exactly as
// before, but named once per framework instance on stderr.
console.warn(`[deprecated] ${CONVERSATION_ROUTING_DEPRECATION_NOTICE}`);

// Generation counters persist across restarts — reusing generation 1's
// agent name after a restart would reopen (and re-seed) the previous
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ export type {
} from './mcpl/index.js';

// Per-channel conversation routing
/** @deprecated Per-channel conversation routing is deprecated and will be removed (anima-research/agent-framework#235). */
export { ConversationRouter, DEFAULT_CLOSURE_PROMPT } from './mcpl/index.js';
/** @deprecated Per-channel conversation routing is deprecated and will be removed (anima-research/agent-framework#235). */
export type {
ConversationRouterConfig,
ConversationBinding,
Expand Down
35 changes: 34 additions & 1 deletion src/mcpl/conversation-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
* a restarted host must NOT reuse generation 1's agent name, or it would
* reopen the previous engagement's Chronicle namespace and re-seed the
* template context on top of the old history.
*
* DEPRECATED (anima-research/agent-framework#235): per-channel conversation
* routing is being retired. Behavior is unchanged for now; the framework
* logs one `[deprecated]` line when it is configured. Removal is a
* follow-up.
*/

import type { ContextStrategy } from '@animalabs/context-manager';
Expand All @@ -49,6 +54,20 @@ export type TriggerRule = 'always' | 'mention';
* inferring on every message of a four-human group DM is a firehose). */
export type ChannelKind = 'dm' | 'groupDm' | 'channel';

/** Logged once by the framework when `FrameworkConfig.conversations` is set. */
export const CONVERSATION_ROUTING_DEPRECATION_NOTICE =
'Per-channel conversation routing (FrameworkConfig.conversations) is deprecated and will ' +
'be removed in a future release. Its \'mention\' bind/trigger rule reads ' +
'metadata.mentioned, which not every channel server sets (discord-mcpl does not), so on ' +
'such channels an @-mention neither binds a fork nor triggers a bound one. Routing still ' +
'works for now. See anima-research/agent-framework#235.';

/**
* @deprecated Per-channel conversation routing is deprecated and will be
* removed (anima-research/agent-framework#235). Its 'mention' bind/trigger
* rule reads `metadata.mentioned`, which not every channel server sets.
* Behavior is unchanged for now.
*/
export interface ConversationRouterConfig {
/** Agent whose context seeds new forks (the "trunk"/warm checkpoint). */
templateAgent: string;
Expand All @@ -61,7 +80,15 @@ export interface ConversationRouterConfig {
* Defaults: dm 'always', groupDm 'mention', channel 'mention'. */
trigger?: { dm?: TriggerRule; groupDm?: TriggerRule; channel?: TriggerRule };

/** Idle time before a binding expires and the fork is closed. Default 12h. */
/**
* Idle time before a binding expires and the fork is closed. Default 12h.
*
* Expiry is checked at most about once a minute, so the sweep usually
* notices an expired binding up to ~60s after the TTL elapses. That is the
* usual extra delay, not a deadline for closing the fork: the closure turn
* can be delayed further (e.g. while the host is quiesced, it stays queued
* until resume).
*/
idleTtlMs?: number;

/** Final system-initiated user message sent to a fork on expiry. */
Expand Down Expand Up @@ -116,6 +143,12 @@ export const DEFAULT_CLOSURE_PROMPT =
// ConversationRouter
// ============================================================================

/**
* @deprecated Per-channel conversation routing is deprecated and will be
* removed (anima-research/agent-framework#235). Behavior is unchanged for
* now; the framework logs one `[deprecated]` line when it constructs a
* router from `FrameworkConfig.conversations`.
*/
export class ConversationRouter {
private config: ConversationRouterConfig;

Expand Down
2 changes: 2 additions & 0 deletions src/mcpl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ export { InferenceRouter } from './inference-router.js';
export { ChannelRegistry } from './channel-registry.js';

// Per-channel conversation routing (fork-per-channel agents)
/** @deprecated Per-channel conversation routing is deprecated and will be removed (anima-research/agent-framework#235). */
export { ConversationRouter, DEFAULT_CLOSURE_PROMPT } from './conversation-router.js';
/** @deprecated Per-channel conversation routing is deprecated and will be removed (anima-research/agent-framework#235). */
export type {
ConversationRouterConfig,
ConversationBinding,
Expand Down
7 changes: 7 additions & 0 deletions src/types/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ export interface FrameworkConfig {
* messages route to per-channel fork agents spawned from the template
* agent instead of the primary conversation. If omitted, behavior is
* unchanged (all messages go to the primary agent).
*
* @deprecated Per-channel conversation routing is deprecated and will be
* removed (anima-research/agent-framework#235). Its 'mention' bind/trigger
* rule reads `metadata.mentioned`, which not every channel server sets
* (discord-mcpl does not), so on those channels an @-mention neither binds
* a fork nor triggers a bound one. Routing still works for now; the
* framework logs one `[deprecated]` line at creation when this is set.
*/
conversations?: ConversationRouterConfig;
}
Expand Down
59 changes: 58 additions & 1 deletion test/conversation-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* conversation when FrameworkConfig.conversations is set.
*/

import { describe, it, beforeEach, afterEach } from 'node:test';
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import { tmpdir } from 'node:os';
import { mkdtempSync, rmSync } from 'node:fs';
Expand Down Expand Up @@ -86,6 +86,63 @@ describe('Conversation routing', () => {
);
});

// Deprecation (agent-framework#235): behavior unchanged, one stderr line.
function deprecationWarnings(warn: { mock: { calls: Array<{ arguments: unknown[] }> } }): string[] {
return warn.mock.calls
.map((c) => String(c.arguments[0]))
.filter((line) => line.startsWith('[deprecated]') && line.includes('conversation routing'));
}

it('configured: logs one [deprecated] line per framework and still routes', async () => {
const warn = mock.method(console, 'warn', () => {});
let framework: AgentFramework | undefined;
try {
framework = await makeFramework();
assert.equal(deprecationWarnings(warn).length, 1, 'warned once at creation');
assert.match(deprecationWarnings(warn)[0]!, /agent-framework#235/);

// Routing is unchanged: a DM and a channel mention each spawn a fork.
// (One queued response per stream: MockMembrane hands a stream every
// response still queued.)
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hi alice!' }]));
framework.pushEvent(incomingEvent({ channelId: 'slack:D1', text: 'hello there', channelType: 'im' }));
await framework.runUntilIdle();
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'On it.' }]));
framework.pushEvent(incomingEvent({ channelId: 'slack:C1', text: 'bot, help', mentioned: true }));
await framework.runUntilIdle();
assert.ok(framework.getAgent('conversation-slack-D1-g1'), 'DM fork spawned');
assert.ok(framework.getAgent('conversation-slack-C1-g1'), 'channel-mention fork spawned');
assert.equal(membrane.calls.length, 2, 'both forks ran inference');

assert.equal(deprecationWarnings(warn).length, 1, 'spawning forks does not warn again');
} finally {
warn.mock.restore();
await framework?.stop();
}
});

it('not configured: no [deprecated] line', async () => {
const warn = mock.method(console, 'warn', () => {});
let framework: AgentFramework | undefined;
try {
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hi alice!' }]));
framework = await AgentFramework.create({
storePath: join(tempDir, 'test.chronicle'),
membrane: membrane.asMembrane(),
agents: [{ name: 'trunk', model: 'test-model', systemPrompt: 'You are the trunk.' }],
modules: [],
});
framework.pushEvent(incomingEvent({ channelId: 'slack:D1', text: 'hello there', channelType: 'im' }));
await framework.runUntilIdle();

assert.equal(framework.getConversationRouter(), null);
assert.deepEqual(deprecationWarnings(warn), []);
} finally {
warn.mock.restore();
await framework?.stop();
}
});

it('DM message spawns a fork, routes the message there, and triggers inference', async () => {
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hi alice!' }]));
const framework = await makeFramework();
Expand Down
Loading