feat: add P2 features — retry policy, prompt templating, usage tracking, tech debt cleanup
- Extract shared splitMessage() into channels/utils.ts (dedup 4 adapters) - Add Slack user name resolution with caching (users.info API) - Add withRetry() with exponential backoff + jitter, isRetryable() filter - Wire retry config into ModelRouter.chat() (non-streaming only) - Add assembleSystemPrompt() multi-file template system (SOUL/AGENTS/IDENTITY/USER/TOOLS.md) - Add usage tracking accumulators in NativeAgent + AgentOrchestrator - Add estimateCost() with per-model pricing table - Add /usage TUI command with full usage report formatting - Add retrySchema and promptSchema to config schema Tests: 569 passing, typecheck clean
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
ChannelAdapter,
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
|
||||
/** Configuration for the Slack channel adapter. */
|
||||
export interface SlackAdapterConfig {
|
||||
@@ -34,36 +35,6 @@ interface SlackMessageEvent {
|
||||
subtype?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a long message into chunks that respect Slack's readability limit.
|
||||
* Prefers splitting at newlines, then spaces, then hard-cuts.
|
||||
*/
|
||||
function splitMessage(text: string, maxLength: number): string[] {
|
||||
const chunks: string[] = [];
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
if (remaining.length <= maxLength) {
|
||||
chunks.push(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to split at a newline within the allowed window
|
||||
let splitIndex = remaining.lastIndexOf('\n', maxLength);
|
||||
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
||||
splitIndex = remaining.lastIndexOf(' ', maxLength);
|
||||
}
|
||||
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
||||
splitIndex = maxLength;
|
||||
}
|
||||
|
||||
chunks.push(remaining.slice(0, splitIndex));
|
||||
remaining = remaining.slice(splitIndex).trimStart();
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack channel adapter backed by @slack/bolt.
|
||||
*
|
||||
@@ -77,6 +48,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
private app: App | null = null;
|
||||
private messageHandler?: (msg: InboundMessage) => void;
|
||||
private config: SlackAdapterConfig;
|
||||
private userNameCache: Map<string, string> = new Map();
|
||||
|
||||
get status(): ChannelStatus {
|
||||
return this._status;
|
||||
@@ -105,7 +77,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
|
||||
// Register message event handler
|
||||
this.app.message(async ({ message }) => {
|
||||
this.handleMessage(message as unknown as SlackMessageEvent);
|
||||
await this.handleMessage(message as unknown as SlackMessageEvent);
|
||||
});
|
||||
|
||||
await this.app.start();
|
||||
@@ -161,8 +133,23 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a Slack user ID to a display name, with caching. */
|
||||
private async resolveUserName(userId: string): Promise<string> {
|
||||
const cached = this.userNameCache.get(userId);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const result = await this.app!.client.users.info({ user: userId });
|
||||
const name = result.user?.real_name || result.user?.name || userId;
|
||||
this.userNameCache.set(userId, name);
|
||||
return name;
|
||||
} catch {
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal: process an inbound Slack message event. */
|
||||
private handleMessage(message: SlackMessageEvent): void {
|
||||
private async handleMessage(message: SlackMessageEvent): Promise<void> {
|
||||
if (!this.messageHandler) return;
|
||||
|
||||
// Ignore bot messages
|
||||
@@ -187,9 +174,10 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
// Strip bot mentions: <@U\w+> pattern
|
||||
let text = (message.text ?? '').replace(/<@U\w+>/g, '').trim();
|
||||
|
||||
// TODO: message.user is a Slack user ID (e.g. U0123ABC), not a display name.
|
||||
// To resolve display names, use this.app.client.users.info() with caching.
|
||||
const senderName = message.user;
|
||||
// Resolve display name from Slack user ID
|
||||
const senderName = message.user
|
||||
? await this.resolveUserName(message.user)
|
||||
: undefined;
|
||||
|
||||
// Detect reset command
|
||||
if (text === '!reset' || text === 'reset') {
|
||||
|
||||
Reference in New Issue
Block a user