Merge remote-tracking branch 'origin/master' into codex/remove-stdio-agent

# Conflicts:
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	packages/core/agent-loop/tests/config-session-id.spec.ts
#	packages/ui/stdio/README.md
#	packages/ui/stdio/src/index.ts
#	packages/ui/stdio/tests/stdio.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-20 20:29:08 +08:00
19 changed files with 272 additions and 66 deletions

View File

@@ -10,7 +10,7 @@ import type { Context } from 'cordis'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import type { AgentOptions, AgentStatus, HookContext, InjectOptions, SendOptions } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { deepFreeze } from '@deepseek-ai/dsh-llm'
import { deepFreeze, errorChain } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue, type Session, type SessionId } from '@deepseek-ai/dsh-session'
import { Inbox, type InboxMessage } from './inbox.ts'
@@ -80,7 +80,6 @@ export function prepareReactLoopAgent(
},
}
}
/**
* Install the concrete agent's scope context exactly once. Construction and
* scope minting are mutually referential (the scope key is the agent), so the
@@ -290,7 +289,7 @@ export class ReactLoopAgent implements Agent {
if (turnRecorded) {
// Through the store's flush (the carrier owner), never a raw parallel.
const flush = this.loopCtx.sessions.flush(this.session).catch((error: unknown) => {
const rendered = renderThrown(error)
const rendered = errorChain(error)
const err = error instanceof Error ? error : new Error(rendered)
this.loopCtx.logger.warn(`agent "${this.id}": flush after idle injection failed: ${rendered}`)
agentEvents(this.loopCtx, this).emit('agent/error', turn, 0, err)
@@ -444,8 +443,3 @@ export class ReactLoopAgent implements Agent {
}
}
}
/** Render an ordinary thrown value for the error event and log. */
function renderThrown(value: unknown): string {
return value instanceof Error ? value.message : String(value)
}

View File

@@ -20,7 +20,7 @@ import type {
ResumeAgentOptions,
SessionStartSource,
} from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-llm'
import { errorChain } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionHeader } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -41,15 +41,6 @@ const INACTIVE_STATES: ReadonlySet<FiberState> = new Set([
FiberState.FAILED,
])
/** Render an arbitrary thrown value without letting coercion escape containment. */
function renderThrown(value: unknown): string {
try {
return String(value)
} catch {
return '<unrenderable thrown value>'
}
}
/** Factory-level ownership of every preparing or live transaction. */
class FactoryOwnership {
private accepting = true
@@ -475,16 +466,16 @@ export class AgentLoop extends Service implements AgentFactory {
error: unknown,
): void {
if (!this.ownership.isActive()) return
this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${renderThrown(error)}`)
this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${errorChain(error)}`)
const args: unknown[] = ['agent-loop/config-start-failed', sessionId, error]
for (const callback of this.ctx.events.dispatch('emit', args)) {
try {
const returned: unknown = callback(...args)
void Promise.resolve(returned).catch((listenerError: unknown) => {
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener rejected: ${renderThrown(listenerError)}`)
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener rejected: ${errorChain(listenerError)}`)
})
} catch (listenerError: unknown) {
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener threw: ${renderThrown(listenerError)}`)
this.ctx.logger.warn(`agent "${configId}": config-start-failed listener threw: ${errorChain(listenerError)}`)
}
}
}

View File

@@ -8,7 +8,7 @@
import type { Context } from 'cordis'
import type { ContentBlock, FinishReason, GenerateOptions, LlmCallConfig, Message } from '@deepseek-ai/dsh-llm'
import { isDeepStrictEqual } from 'node:util'
import { BlockAssembler, HarnessError, assertNever, deepFreeze, isLlmAdapterFailure } from '@deepseek-ai/dsh-llm'
import { BlockAssembler, HarnessError, assertNever, deepFreeze, errorChain, isLlmAdapterFailure } from '@deepseek-ai/dsh-llm'
import { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { AgentEventDispatch, ContinuationDecision, HookContext, PromptDecision, RequestError, RequestErrorDecision } from '@deepseek-ai/dsh-agent'
import { canonicalHeader } from '@deepseek-ai/dsh-session'
@@ -56,9 +56,12 @@ function finishError(finish: FinishReason): RequestError | undefined {
/**
* Build the `{ message, code? }` part of an error payload, omitting the
* `code` key entirely when absent (exactOptionalPropertyTypes-correct).
* The durable message renders the full cause chain: `turn/end` is the single
* durable record of an in-turn failure, so a wrapper message alone (e.g.
* `fetch failed`) would lose the diagnosis the session log exists to keep.
*/
function errorData(err: RequestError): { message: string; code?: string } {
return { message: err.message, ...typeof err.code === 'string' ? { code: err.code } : {} }
return { message: errorChain(err), ...typeof err.code === 'string' ? { code: err.code } : {} }
}
/** Map a successful max-token finish onto the turn reason; other successful finishes add nothing. */
@@ -166,7 +169,7 @@ export async function runLoop(ctx: Context, handle: LoopHandle): Promise<void> {
} catch (error: unknown) {
// Pre-turn failure has no durable boundary to close; report it without appending outside a turn.
const err = toError(error)
ctx.logger.warn(`agent "${agent.id}": turn ${turn} failed before it started: ${err.message}`)
ctx.logger.warn(`agent "${agent.id}": turn ${turn} failed before it started: ${errorChain(err)}`)
try {
events.emit('agent/error', turn, 0, err)
} catch { /* contained: a throwing agent/error listener must not kill the driver */ }
@@ -382,7 +385,7 @@ async function runTurn(
)
} catch (recoveryError: unknown) {
ctx.logger.warn(
`agent "${agent.id}": request recovery failed at turn ${turn}, step ${step}: ${toError(recoveryError).message}`,
`agent "${agent.id}": request recovery failed at turn ${turn}, step ${step}: ${errorChain(recoveryError)}`,
)
}
handle.setAbort(undefined)
@@ -546,7 +549,7 @@ async function runTurn(
} catch (error: unknown) {
// The turn is closed, so report the failed flush live rather than append outside a turn.
const err = toError(error)
ctx.logger.warn(`agent "${agent.id}": session/flush failed at turn ${turn}: ${err.message}`)
ctx.logger.warn(`agent "${agent.id}": session/flush failed at turn ${turn}: ${errorChain(err)}`)
try {
events.emit('agent/error', turn, step, err)
} catch {

View File

@@ -217,14 +217,14 @@ describe('config-driven session id', () => {
})
await expect.poll(() => warn).toHaveBeenCalledWith(expect.stringContaining(
'config-driven restore of "config-exact-failure" failed: Error: persistence index failed',
'config-driven restore of "config-exact-failure" failed: persistence index failed',
))
expect(failures).toEqual([{ sessionId: SessionId('config-exact-failure'), error: failure }])
expect(warn).toHaveBeenCalledWith(
'agent "main": config-start-failed listener threw: Error: failure observer failed',
'agent "main": config-start-failed listener threw: failure observer failed',
)
await expect.poll(() => warn).toHaveBeenCalledWith(
'agent "main": config-start-failed listener rejected: Error: async failure observer failed',
'agent "main": config-start-failed listener rejected: async failure observer failed',
)
expect(ctx.agents.get(SessionId('config-exact-failure'))).toBeUndefined()
warn.mockRestore()
@@ -256,13 +256,13 @@ describe('config-driven session id', () => {
await expect.poll(() => failures).toEqual([unrenderable])
expect(warn).toHaveBeenCalledWith(
'agent "main": config-driven restore of "config-exact-unrenderable" failed: <unrenderable thrown value>',
'agent "main": config-driven restore of "config-exact-unrenderable" failed: <unrenderable value>',
)
expect(warn).toHaveBeenCalledWith(
'agent "main": config-start-failed listener threw: <unrenderable thrown value>',
'agent "main": config-start-failed listener threw: <unrenderable value>',
)
await expect.poll(() => warn).toHaveBeenCalledWith(
'agent "main": config-start-failed listener rejected: <unrenderable thrown value>',
'agent "main": config-start-failed listener rejected: <unrenderable value>',
)
await ctx.fiber.dispose()
})