refactor(agent): unify sourced message delivery

This commit is contained in:
_Kerman
2026-07-24 22:38:50 +08:00
parent 009d113e0e
commit 992cf894af
197 changed files with 1890 additions and 2132 deletions
+19 -10
View File
@@ -15,8 +15,9 @@
import { readFileSync } from 'node:fs'
import type { Context } from 'cordis'
import z from 'schemastery'
import type { AdditionalContext, Agent, PromptDecision } from '@deepseek-ai/dsh-agent'
import type { Agent, PromptDecision } from '@deepseek-ai/dsh-agent'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import type { UserMessageData } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-persistence'
import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import {
@@ -102,6 +103,12 @@ export function apply(ctx: Context, config: Config): void {
const detached = createDetachedRuns()
ctx.effect(() => () => detached.drain(), 'hooks-codex: drain detached hook runs')
/**
* Run and fold one configured Codex hook point.
*
* A supplied turn records the hook provenance pair inside that open turn.
* Pre-turn `UserPromptSubmit` and detached lifecycle points omit it.
*/
async function runPoint(
point: string,
matchQuery: string,
@@ -163,14 +170,14 @@ export function apply(ctx: Context, config: Config): void {
// TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt seam.
function contextFrom(merged: MergedHookOutcome): AdditionalContext | undefined {
function contextFrom(merged: MergedHookOutcome): UserMessageData | undefined {
if (merged.additionalContext.length === 0) return undefined
const content: ContentBlock[] = merged.additionalContext.map(text => ({ type: 'text', text }))
return { content, source: PLUGIN_SOURCE }
}
/** Prepend one context without flattening downstream provenance or metadata. */
function prependContext(ours: AdditionalContext, theirs: AdditionalContext[] | undefined): AdditionalContext[] {
function prependContext(ours: UserMessageData, theirs: UserMessageData[] | undefined): UserMessageData[] {
return [ours, ...theirs ?? []]
}
@@ -181,7 +188,7 @@ export function apply(ctx: Context, config: Config): void {
detached.track(runPoint('SessionStart', source, { ...base(ctx, agent, 'SessionStart', model), source }, { agent, plainStdoutAsContext: true, signal: detached.signal })
.then((merged) => {
const context = contextFrom(merged)
if (context) agent.inject(context.content, { source: context.source })
if (context) agent.inject({ content: context.content, source: context.source })
})
.catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) }))
/* jscpd:ignore-end */
@@ -189,8 +196,12 @@ export function apply(ctx: Context, config: Config): void {
// UserPromptSubmit → PromptDecision. Codex supports block, not allow or ask.
ctx.on('agent/prompt-submit', async (agent, content, _source, signal, next): Promise<PromptDecision> => {
const turn = lastTurn(agent)
const merged = await runPoint('UserPromptSubmit', '', { ...turnBase(ctx, agent, 'UserPromptSubmit', model), prompt: blocksToText(content) }, { agent, turn, plainStdoutAsContext: true, signal })
const payload = {
...base(ctx, agent, 'UserPromptSubmit', model),
turn_id: String(lastTurn(agent) + 1),
prompt: blocksToText(content),
}
const merged = await runPoint('UserPromptSubmit', '', payload, { agent, plainStdoutAsContext: true, signal })
/* jscpd:ignore-start */
if (merged.decision === 'deny') return { kind: 'block', reason: merged.reason ?? 'blocked by UserPromptSubmit hook' }
// Context alone is not a veto: DELEGATE so a later prompt-submit listener can
@@ -249,7 +260,7 @@ export function apply(ctx: Context, config: Config): void {
// empty stderr) still forces it — fall back to a generic steering line
// rather than letting the turn stop.
const text = merged.reason ?? 'continue: blocked by Stop hook'
agent.steer([{ type: 'text', text }], { source: PLUGIN_SOURCE })
agent.steer({ content: [{ type: 'text', text }], source: PLUGIN_SOURCE })
}
})
}
@@ -263,9 +274,7 @@ export function apply(ctx: Context, config: Config): void {
function lastTurn(agent: Agent | undefined): number {
if (!agent) return 0
const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
/* v8 ignore next -- the `: 0` arm is a defensive fallback: when an agent is
present, lastTurn is only called from the mid-turn seams, which always run
inside an open turn, so `last` is always a turn/start here. */
/* v8 ignore next -- agent-present turnBase callers are tool/stop seams inside an open turn. */
return last?.type === 'turn/start' ? last.data.turn : 0
}