refactor(agent): unify sourced message delivery
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user