refactor agent pre-step inbox lifecycle

This commit is contained in:
_Kerman
2026-07-31 19:21:16 +08:00
parent c2ff9ddec8
commit fcc2b5e282
267 changed files with 2052 additions and 1546 deletions

View File

@@ -7,7 +7,7 @@
import { createHash } from 'node:crypto'
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { assertNever, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
@@ -134,22 +134,45 @@ export function apply(ctx: Context, config: Config = {}): void {
// Register after the tool so reverse teardown removes guidance first. Exact definition
// identity prevents a scoped shadow merely named `skill` from inheriting this catalog.
ctx.on('agent/step', async (agent: Agent, _turn, _step, signal): Promise<void> => {
ctx.on('agent/pre-step', async (
agent: Agent,
_messages,
{ signal },
next,
): Promise<PreStepDecision> => {
const decision = await next()
if (decision.kind === 'reject') return decision
signal.throwIfAborted()
const toolVisible = ctx.tools.get(skillTool.name, agent) === registeredSkillTool
const snapshot = toolVisible
? await ctx.skills.snapshot({ cwd: agent.session.header.cwd, signal })
: { skills: [], complete: true }
signal.throwIfAborted()
if (!snapshot.complete) return
if (!snapshot.complete) return decision
const skills = snapshot.skills.filter(isModelInvocable)
const digest = catalogDigest(skills, catalogDescriptionMaxLength)
const history = catalogHistory(agent)
if (history.visibleDigest === digest) return
if (!history.published && skills.length === 0) return
const existing = catalogMessage(decision.messages)
if (history.visibleDigest === digest) {
return existing === undefined
? decision
: { kind: 'enter', messages: decision.messages.filter(message => message.id !== existing.id) }
}
if (existing !== undefined && catalogContentDigest(existing.content) === digest) return decision
if (!history.published && skills.length === 0) {
return existing === undefined
? decision
: { kind: 'enter', messages: decision.messages.filter(message => message.id !== existing.id) }
}
const catalog = history.published
? renderCatalogUpdate(skills, catalogDescriptionMaxLength)
: renderCatalogMessage(skills, catalogDescriptionMaxLength)
agent.session.append('user/message', catalog, { surfaceOp: 'append' })
return {
kind: 'enter',
messages: existing === undefined
? [...decision.messages, catalog]
: decision.messages.map(message => message.id === existing.id ? catalog : message),
}
})
}
@@ -281,6 +304,13 @@ function catalogHistory(agent: Agent): { visibleDigest?: string; published: bool
return { published }
}
function catalogMessage(messages: readonly UserMessage[]): UserMessage | undefined {
return messages.find(message =>
message.source.kind === 'plugin'
&& message.source.plugin === PLUGIN_SOURCE.plugin
&& catalogContentDigest(message.content) !== undefined)
}
function catalogContentDigest(content: UserMessage['content']): string | undefined {
if (content.length !== 1 || content[0]?.type !== 'text') return undefined
const text = content[0].text