refactor agent pre-step inbox lifecycle
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user