refactor(channels): add shared turn kernel

This commit is contained in:
Peter Steinberger
2026-04-29 22:28:47 +01:00
parent 4396361f35
commit 9a9cd0c0ab
62 changed files with 3449 additions and 1333 deletions

View File

@@ -175,6 +175,7 @@ function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): Plu
session: {
readSessionUpdatedAt: readSessionUpdatedAtMock,
resolveStorePath: resolveStorePathMock,
recordInboundSession: vi.fn(async () => undefined),
},
reply: {
resolveEnvelopeFormatOptions:
@@ -196,6 +197,11 @@ function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): Plu
upsertPairingRequest: vi.fn(),
buildPairingReply: vi.fn(),
},
turn: {
runPrepared: vi.fn(async (params) => ({
dispatchResult: await params.runDispatch(),
})),
},
...overrides.channel,
},
...(overrides.system ? { system: overrides.system as PluginRuntime["system"] } : {}),

View File

@@ -1268,8 +1268,18 @@ export async function handleFeishuMessage(params: {
}
const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, {
agentId,
});
const agentRecord = {
onRecordError: (err: unknown) => {
log(
`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`,
);
},
};
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
storePath: core.channel.session.resolveStorePath(cfg.session?.store, { agentId }),
storePath: agentStorePath,
sessionKey: agentSessionKey,
});
const agentCtx = await buildCtxPayloadForAgent(
@@ -1302,15 +1312,30 @@ export async function handleFeishuMessage(params: {
log(
`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`,
);
await core.channel.reply.withReplyDispatcher({
dispatcher,
onSettled: () => markDispatchIdle(),
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: agentCtx,
cfg,
await core.channel.turn.runPrepared({
channel: "feishu",
accountId: route.accountId,
routeSessionKey: agentSessionKey,
storePath: agentStorePath,
ctxPayload: agentCtx,
recordInboundSession: core.channel.session.recordInboundSession,
record: agentRecord,
onPreDispatchFailure: () =>
core.channel.reply.settleReplyDispatcher({
dispatcher,
replyOptions,
onSettled: () => markDispatchIdle(),
}),
runDispatch: () =>
core.channel.reply.withReplyDispatcher({
dispatcher,
onSettled: () => markDispatchIdle(),
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: agentCtx,
cfg,
dispatcher,
replyOptions,
}),
}),
});
} else {
@@ -1331,13 +1356,23 @@ export async function handleFeishuMessage(params: {
log(
`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`,
);
await core.channel.reply.withReplyDispatcher({
dispatcher: noopDispatcher,
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: agentCtx,
cfg,
await core.channel.turn.runPrepared({
channel: "feishu",
accountId: route.accountId,
routeSessionKey: agentSessionKey,
storePath: agentStorePath,
ctxPayload: agentCtx,
recordInboundSession: core.channel.session.recordInboundSession,
record: agentRecord,
runDispatch: () =>
core.channel.reply.withReplyDispatcher({
dispatcher: noopDispatcher,
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: agentCtx,
cfg,
dispatcher: noopDispatcher,
}),
}),
});
}
@@ -1385,10 +1420,11 @@ export async function handleFeishuMessage(params: {
);
const identity = resolveAgentOutboundIdentity(cfg, route.agentId);
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
storePath: core.channel.session.resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
}),
storePath,
sessionKey: route.sessionKey,
});
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
@@ -1409,19 +1445,41 @@ export async function handleFeishuMessage(params: {
});
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
dispatcher,
onSettled: () => {
markDispatchIdle();
const { dispatchResult } = await core.channel.turn.runPrepared({
channel: "feishu",
accountId: route.accountId,
routeSessionKey: route.sessionKey,
storePath,
ctxPayload,
recordInboundSession: core.channel.session.recordInboundSession,
record: {
onRecordError: (err) => {
log(
`feishu[${account.accountId}]: failed to record inbound session ${route.sessionKey}: ${String(err)}`,
);
},
},
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: ctxPayload,
cfg,
onPreDispatchFailure: () =>
core.channel.reply.settleReplyDispatcher({
dispatcher,
replyOptions,
onSettled: () => markDispatchIdle(),
}),
runDispatch: () =>
core.channel.reply.withReplyDispatcher({
dispatcher,
onSettled: () => {
markDispatchIdle();
},
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: ctxPayload,
cfg,
dispatcher,
replyOptions,
}),
}),
});
const { queuedFinal, counts } = dispatchResult;
if (isGroup && historyKey && chatHistories) {
clearHistoryEntriesIfEnabled({

View File

@@ -86,6 +86,27 @@ function createTestRuntime(overrides?: {
},
);
const recordInboundSession = vi.fn(async () => {});
const runPrepared = vi.fn(
async (turn: Parameters<PluginRuntime["channel"]["turn"]["runPrepared"]>[0]) => {
await turn.recordInboundSession({
storePath: turn.storePath,
sessionKey: turn.ctxPayload.SessionKey ?? turn.routeSessionKey,
ctx: turn.ctxPayload,
groupResolution: turn.record?.groupResolution,
createIfMissing: turn.record?.createIfMissing,
updateLastRoute: turn.record?.updateLastRoute,
onRecordError: turn.record?.onRecordError ?? (() => undefined),
});
const dispatchResult = await turn.runDispatch();
return {
admission: { kind: "dispatch" as const },
dispatched: true,
ctxPayload: turn.ctxPayload,
routeSessionKey: turn.routeSessionKey,
dispatchResult,
};
},
);
return {
channel: {
@@ -112,6 +133,11 @@ function createTestRuntime(overrides?: {
resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json"),
recordInboundSession,
},
turn: {
runPrepared: runPrepared as unknown as PluginRuntime["channel"]["turn"]["runPrepared"],
dispatchAssembled:
vi.fn() as unknown as PluginRuntime["channel"]["turn"]["dispatchAssembled"],
},
pairing: {
readAllowFromStore: vi.fn(overrides?.readAllowFromStore ?? (async () => [])),
upsertPairingRequest: vi.fn(

View File

@@ -221,16 +221,6 @@ export async function handleFeishuCommentEvent(
const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, {
agentId: route.agentId,
});
await core.channel.session.recordInboundSession({
storePath,
sessionKey: commentSessionKey,
ctx: ctxPayload,
onRecordError: (err) => {
error(
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
);
},
});
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } =
createFeishuCommentReplyDispatcher({
@@ -245,28 +235,59 @@ export async function handleFeishuCommentEvent(
isWholeComment: turn.isWholeComment,
});
let dispatchSettledBeforeStart = false;
try {
log(
`feishu[${account.accountId}]: dispatching drive comment to agent ` +
`(session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`,
);
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
dispatcher,
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: ctxPayload,
cfg: effectiveCfg,
const { dispatchResult } = await core.channel.turn.runPrepared({
channel: "feishu",
accountId: route.accountId,
routeSessionKey: commentSessionKey,
storePath,
ctxPayload,
recordInboundSession: core.channel.session.recordInboundSession,
record: {
onRecordError: (err) => {
error(
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
);
},
},
onPreDispatchFailure: async () => {
dispatchSettledBeforeStart = true;
await core.channel.reply.settleReplyDispatcher({
dispatcher,
replyOptions,
onSettled: () => {
markRunComplete();
markDispatchIdle();
},
});
},
runDispatch: () =>
core.channel.reply.withReplyDispatcher({
dispatcher,
run: () =>
core.channel.reply.dispatchReplyFromConfig({
ctx: ctxPayload,
cfg: effectiveCfg,
dispatcher,
replyOptions,
}),
}),
});
const queuedFinal = dispatchResult?.queuedFinal ?? false;
const counts = dispatchResult?.counts ?? { tool: 0, block: 0, final: 0 };
log(
`feishu[${account.accountId}]: drive comment dispatch complete ` +
`(queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`,
);
} finally {
markRunComplete();
markDispatchIdle();
if (!dispatchSettledBeforeStart) {
markRunComplete();
markDispatchIdle();
}
void cleanupTypingReaction();
}
}