feat: expose agent session log location

This commit is contained in:
Yichen Jiang
2026-07-10 20:52:27 +08:00
parent 42ebbfdf8f
commit eea0a99985
40 changed files with 526 additions and 70 deletions
+24 -20
View File
@@ -27,6 +27,7 @@ import type { Context } from 'cordis'
import z from 'schemastery'
import type { Agent, ContinuationDecision, HookContext, PromptDecision } from '@deepseek-ai/dsh-agent'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-session-persistence'
import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import {
appendHookInvoked,
@@ -246,7 +247,7 @@ export function apply(ctx: Context, config: Config): void {
// to the interception seams; today the contract is "injected as soon as the
// hook resolves", not "before the first request". ---
ctx.on('agent/session-start', (agent, source) => {
detached.track(runPoint('SessionStart', source, sessionStartPayload(agent, source), { agent, signal: detached.signal })
detached.track(runPoint('SessionStart', source, sessionStartPayload(ctx, agent, source), { agent, signal: detached.signal })
.then((merged) => {
const context = contextFrom(merged)
if (context) agent.inject(context.content, { source: context.source })
@@ -260,7 +261,7 @@ export function apply(ctx: Context, config: Config): void {
// matcher subject (CC ignores matchers for this event). ---
ctx.on('agent/prompt-submit', async (agent, content, _source, next): Promise<PromptDecision> => {
const turn = lastTurn(agent)
const merged = await runPoint('UserPromptSubmit', '', promptPayload(agent, content), { agent, turn })
const merged = await runPoint('UserPromptSubmit', '', promptPayload(ctx, agent, content), { agent, turn })
if (merged.decision === 'deny') {
return { kind: 'block', reason: merged.reason ?? 'blocked by UserPromptSubmit hook' }
}
@@ -281,7 +282,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PreToolUse → PreToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
const turn = lastTurn(exec.agent)
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' }
if (merged.decision === 'ask') return { kind: 'ask', ...merged.reason !== undefined ? { reason: merged.reason } : {} }
return next()
@@ -290,7 +291,7 @@ export function apply(ctx: Context, config: Config): void {
// --- PostToolUse → PostToolDecision. Matcher subject is the tool name. ---
ctx.on('tools/post-execute', async (exec, result, next): Promise<PostToolDecision> => {
const turn = lastTurn(exec.agent)
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, ...exec.signal ? { signal: exec.signal } : {} })
const context = contextFrom(merged)
if (merged.decision === 'deny') {
return { kind: 'block', feedback: [{ type: 'text', text: merged.reason ?? 'blocked by PostToolUse hook' }], ...context ? { additionalContext: context } : {} }
@@ -317,7 +318,7 @@ export function apply(ctx: Context, config: Config): void {
// false, so a Stop hook that unconditionally blocks would force-continue every
// step — a hook author must self-limit until the guard lands. ---
ctx.on('agent/turn-continuation', async (agent, turn, _default, next): Promise<ContinuationDecision> => {
const merged = await runPoint('Stop', '', stopPayload(agent), { agent, turn })
const merged = await runPoint('Stop', '', stopPayload(ctx, agent), { agent, turn })
if (merged.decision === 'deny') {
// A blocking Stop hook forces continuation. It carries its reason as
// next-step steering; a blocking hook that emitted no reason (exit 2, empty
@@ -339,7 +340,7 @@ export function apply(ctx: Context, config: Config): void {
// a specific-kind matcher does not (documented in the RFC). ---
ctx.on('subagent/start', (info) => {
const child = ctx.get('agents')?.get(info.id)
detached.track(runPoint('SubagentStart', SUBAGENT_TYPE, subagentPayload('SubagentStart', info, child), { ...child ? { agent: child } : {}, signal: detached.signal })
detached.track(runPoint('SubagentStart', SUBAGENT_TYPE, subagentPayload(ctx, 'SubagentStart', info, child), { ...child ? { agent: child } : {}, signal: detached.signal })
.then((merged) => {
const context = contextFrom(merged)
if (context && child) child.inject(context.content, { source: context.source })
@@ -355,7 +356,7 @@ export function apply(ctx: Context, config: Config): void {
// reject — no `.catch` is needed (the tracker's settlement bookkeeping
// would absorb one anyway).
const child = ctx.get('agents')?.get(info.id)
detached.track(runPoint('SubagentStop', SUBAGENT_TYPE, subagentPayload('SubagentStop', info, child), { ...child ? { agent: child } : {}, signal: detached.signal }))
detached.track(runPoint('SubagentStop', SUBAGENT_TYPE, subagentPayload(ctx, 'SubagentStop', info, child), { ...child ? { agent: child } : {}, signal: detached.signal }))
})
}
@@ -385,28 +386,31 @@ function blocksToText(content: ContentBlock[]): string {
return content.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text').map(b => b.text).join('')
}
function base(agent: Agent | undefined, event: string): Record<string, unknown> {
function base(ctx: Context, agent: Agent | undefined, event: string): Record<string, unknown> {
return {
session_id: agent?.session.header.id ?? '',
transcript_path: agent === undefined
? ''
: ctx.get('sessionPersistence')?.locate(agent.session.header)?.path ?? '',
cwd: agent?.session.header.cwd ?? process.cwd(),
hook_event_name: event,
}
}
function sessionStartPayload(agent: Agent, source: string): Record<string, unknown> {
return { ...base(agent, 'SessionStart'), source }
function sessionStartPayload(ctx: Context, agent: Agent, source: string): Record<string, unknown> {
return { ...base(ctx, agent, 'SessionStart'), source }
}
function promptPayload(agent: Agent, content: ContentBlock[]): Record<string, unknown> {
return { ...base(agent, 'UserPromptSubmit'), prompt: blocksToText(content) }
function promptPayload(ctx: Context, agent: Agent, content: ContentBlock[]): Record<string, unknown> {
return { ...base(ctx, agent, 'UserPromptSubmit'), prompt: blocksToText(content) }
}
function preToolPayload(exec: ToolExecution): Record<string, unknown> {
return { ...base(exec.agent, 'PreToolUse'), tool_name: exec.name, tool_input: exec.arguments, tool_use_id: exec.callId }
function preToolPayload(ctx: Context, exec: ToolExecution): Record<string, unknown> {
return { ...base(ctx, exec.agent, 'PreToolUse'), tool_name: exec.name, tool_input: exec.arguments, tool_use_id: exec.callId }
}
function postToolPayload(exec: ToolExecution, result: ToolExecutionResult): Record<string, unknown> {
return { ...base(exec.agent, 'PostToolUse'), tool_name: exec.name, tool_input: exec.arguments, tool_use_id: exec.callId, tool_response: blocksToText(result.content) }
function postToolPayload(ctx: Context, exec: ToolExecution, result: ToolExecutionResult): Record<string, unknown> {
return { ...base(ctx, exec.agent, 'PostToolUse'), tool_name: exec.name, tool_input: exec.arguments, tool_use_id: exec.callId, tool_response: blocksToText(result.content) }
}
function stopPayload(agent: Agent): Record<string, unknown> {
return { ...base(agent, 'Stop'), stop_hook_active: false }
function stopPayload(ctx: Context, agent: Agent): Record<string, unknown> {
return { ...base(ctx, agent, 'Stop'), stop_hook_active: false }
}
/**
* Build a SubagentStart/SubagentStop payload from the CC base (the child's
@@ -414,9 +418,9 @@ function stopPayload(agent: Agent): Record<string, unknown> {
* fields. `agent_type` is the CC-default {@link SUBAGENT_TYPE}; `stop_hook_active`
* is present on SubagentStop only (the loop-guard flag, always false this cut).
*/
function subagentPayload(event: 'SubagentStart' | 'SubagentStop', info: { id: string }, child: Agent | undefined): Record<string, unknown> {
function subagentPayload(ctx: Context, event: 'SubagentStart' | 'SubagentStop', info: { id: string }, child: Agent | undefined): Record<string, unknown> {
return {
...base(child, event),
...base(ctx, child, event),
agent_id: info.id,
agent_type: SUBAGENT_TYPE,
...event === 'SubagentStop' ? { stop_hook_active: false } : {},