Merge remote-tracking branch 'origin/master' into worktree/context-source-cards
# Conflicts: # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md # apps/web/tests/seeded-history.e2e.ts # apps/web/tests/snapshots/queue-actions/layout.expected.md # docs/core-data-structures/core.i18n.yaml # examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/headless-agent/tests/snapshots/pty-tools/session.jsonl # packages/client/runtime/README.i18n.yaml # packages/client/runtime/tests/history-fold.spec.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx # packages/context/workspace-context/src/index.ts # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/skill/tool-skill/README.i18n.yaml # packages/skill/tool-skill/README.md # packages/skill/tool-skill/README.zh.md # packages/skill/tool-skill/src/index.ts # packages/skill/tool-skill/tests/tool-skill.spec.ts
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'
|
||||
@@ -162,23 +162,46 @@ 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 entries = catalogSourceEntries(skills, catalogDescriptionMaxLength)
|
||||
const digest = digestCatalogEntries(entries)
|
||||
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.message.id) }
|
||||
}
|
||||
if (existing !== undefined && digestCatalogEntries(existing.entries) === digest) return decision
|
||||
if (!history.published && skills.length === 0) {
|
||||
return existing === undefined
|
||||
? decision
|
||||
: { kind: 'enter', messages: decision.messages.filter(message => message.id !== existing.message.id) }
|
||||
}
|
||||
const catalog = history.published
|
||||
? renderCatalogUpdate(entries)
|
||||
: renderCatalogMessage(entries)
|
||||
agent.inject(catalog)
|
||||
return {
|
||||
kind: 'enter',
|
||||
messages: existing === undefined
|
||||
? [...decision.messages, catalog]
|
||||
: decision.messages.map(message => message.id === existing.message.id ? catalog : message),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -350,6 +373,17 @@ function catalogHistory(agent: Agent): { visibleDigest?: string; published: bool
|
||||
return { published }
|
||||
}
|
||||
|
||||
function catalogMessage(
|
||||
messages: readonly UserMessage[],
|
||||
): { message: UserMessage; entries: SkillCatalogSource['entries'] } | undefined {
|
||||
for (const message of messages) {
|
||||
if (message.source.kind !== 'skill-catalog') continue
|
||||
const entries = readCatalogEntries(message.source)
|
||||
if (entries !== undefined) return { message, entries }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Normalized, length-bounded description exactly as the catalog publishes it (unescaped). */
|
||||
function catalogDescription(value: string, maxLength: number): string {
|
||||
const normalized = value.replaceAll(/\s+/g, ' ').trim()
|
||||
|
||||
Reference in New Issue
Block a user