Merge latest agent execution context base
This commit is contained in:
@@ -18,6 +18,7 @@ import type { TransmissionLog } from './request-log.ts'
|
||||
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
import type { ReactLoopAgent } from './agent.ts'
|
||||
import type { Inbox } from './inbox.ts'
|
||||
import type { TurnCancellation } from './cancellation.ts'
|
||||
@@ -100,6 +101,8 @@ function interruptionTurnEndReason(handle: LoopHandle, signal: AbortSignal): Tur
|
||||
export interface LoopHandle {
|
||||
/** Native-private agent inbox handed to the driver only at internal startup. */
|
||||
readonly inbox: Inbox
|
||||
/** Maximum parallel-safe calls allowed in one step. */
|
||||
readonly maxParallelToolCalls: number
|
||||
setStatus(status: 'idle' | 'running'): void
|
||||
/** Install a fresh active-turn owner before the running notification. */
|
||||
installTurnCancellation(): TurnCancellation
|
||||
@@ -114,6 +117,8 @@ export interface LoopHandle {
|
||||
clearPreRunCancel(): void
|
||||
/** Settle idle waiters when pre-running cancellation skips a turn, without emitting `agent/status`. */
|
||||
settleIdle(): void
|
||||
/** Run an active tool-call batch, accepting post-tool context into the FIFO drained before settlement. */
|
||||
readonly withToolBatch: <T>(run: (acceptContext: (context: HookContext) => void) => Promise<T>) => Promise<T>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,7 +339,7 @@ async function runTurn(
|
||||
let stepOutcome: { hadToolCalls: boolean; finish: FinishReason } | { error: Error }
|
||||
try {
|
||||
stepOutcome = await runStep(
|
||||
ctx, events, agent, turn, step, assembly, fullSystemPrompt, boundaryMessages, transmission, signal)
|
||||
ctx, events, agent, handle, turn, step, assembly, fullSystemPrompt, boundaryMessages, transmission, signal)
|
||||
} catch (error: unknown) {
|
||||
stepOutcome = { error: toError(error) }
|
||||
}
|
||||
@@ -459,6 +464,7 @@ async function runStep(
|
||||
ctx: Context,
|
||||
events: AgentEventDispatch,
|
||||
agent: ReactLoopAgent,
|
||||
handle: LoopHandle,
|
||||
turn: number,
|
||||
step: number,
|
||||
assembly: PromptAssembly,
|
||||
@@ -548,53 +554,15 @@ async function runStep(
|
||||
// empty chunk provenance for a contentless, usage-less provider response.
|
||||
recordAssistantMessage(session, turn, step, header.config, assembledContent, message, assembler, chunkSeqs)
|
||||
|
||||
// Tool execution stays sequential; recheck abort around each normalized result.
|
||||
// Dispatch may overlap; policy, durable results, and result context stay model-ordered.
|
||||
const toolCalls = message.content.filter(block => block.type === 'tool-call')
|
||||
// Buffer context until all results are appended to preserve call/result adjacency.
|
||||
const pendingContext: HookContext[] = []
|
||||
for (const call of toolCalls) {
|
||||
interruptionCheckpoint(signal)
|
||||
const callEvent = session.append('tool/call', { turn, step, callId: call.id, name: call.name, arguments: call.arguments })
|
||||
let parsedArguments: unknown
|
||||
try {
|
||||
parsedArguments = call.arguments ? JSON.parse(call.arguments) : {}
|
||||
} catch {
|
||||
parsedArguments = call.arguments
|
||||
}
|
||||
// TODO(pre-tool-input-rewrite): Keep logged history and live presentation aligned;
|
||||
// see docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md.
|
||||
const result = await ctx.tools.execute({
|
||||
callId: call.id,
|
||||
name: call.name,
|
||||
arguments: parsedArguments,
|
||||
agent,
|
||||
signal,
|
||||
})
|
||||
session.append('tool/result', {
|
||||
turn, step,
|
||||
// Correlation comes from the immutable execution input; the result does
|
||||
// not duplicate this authoritative transcript identity.
|
||||
callId: call.id,
|
||||
content: result.content,
|
||||
isError: result.isError,
|
||||
...result.error ? { error: result.error } : {},
|
||||
// Persist tool-owned presentation data for replay.
|
||||
...result.meta !== undefined ? { meta: result.meta } : {},
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [callEvent.seq] })
|
||||
pendingContext.push(...result.additionalContexts ?? [])
|
||||
interruptionCheckpoint(signal)
|
||||
}
|
||||
|
||||
// Append buffered context after the complete result batch.
|
||||
for (const context of pendingContext) {
|
||||
agent.inject(context.content, {
|
||||
source: context.source,
|
||||
...context.envelope !== undefined ? { envelope: context.envelope } : {},
|
||||
...context.meta !== undefined ? { meta: context.meta } : {},
|
||||
})
|
||||
}
|
||||
|
||||
return { hadToolCalls: toolCalls.length > 0, finish: assembler.finish }
|
||||
if (toolCalls.length === 0) return { hadToolCalls: false, finish: assembler.finish }
|
||||
return handle.withToolBatch(async (acceptContext) => {
|
||||
await executeToolCalls(
|
||||
ctx, agent, turn, step, toolCalls, signal, handle.maxParallelToolCalls, acceptContext,
|
||||
)
|
||||
return { hadToolCalls: true, finish: assembler.finish }
|
||||
})
|
||||
}
|
||||
|
||||
/** Preserve successful-call accounting without retaining output that result processing rejected. */
|
||||
|
||||
Reference in New Issue
Block a user