feat(llm): structured error taxonomy with a shared HarnessError base (RFC 005 pt 2)

Introduce HarnessError in dsh-llm (the leaf package): a stable machine-routable
code distinct from the message, cause chaining, name from the subclass, plus
isHarnessError. LlmError, ToolArgsError, and InvariantError now extend it.

Tool failures carry the structure end-to-end: ToolExecutionResult gains
error: { name, code } (populated from a thrown HarnessError), and the loop
forwards it onto the tool/result session event (which gained the same optional
field) for retry/sandbox plugins and replay. The loop's toError wraps non-Error
throws in a HarnessError(code: UNKNOWN, cause) instead of a bare Error.

Landed last and in isolation so it's a pure upgrade over the plain Error+code
the earlier PRs used — independently revertible. Graduates RFC 005 pt 2 ->
ADR 0015; RFC 005 now fully implemented.
This commit is contained in:
Tianyi Cui
2026-06-14 01:07:28 +08:00
parent 7a39616a06
commit 825b57aff9
18 changed files with 224 additions and 32 deletions

View File

@@ -20,7 +20,7 @@
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ToolDefinition, ToolExecution } from './index.ts'
// ---------------------------------------------------------------------------
@@ -174,20 +174,17 @@ export function schemaSpecToJsonSchema(spec: SchemaSpec): JsonSchemaObject {
/**
* Thrown by a {@link defineTool} tool when the model-generated arguments don't
* match the declared {@link SchemaSpec}. The registry's execute waterfall
* catches it and returns an `isError` result so the model can self-correct.
*
* Plain `Error` for now (carries a `code` field); a later change promotes the
* harness error taxonomy and this extends a common base.
* match the declared {@link SchemaSpec}. Extends {@link HarnessError}
* (`code: 'INVALID_ARGS'`); the registry's execute waterfall catches it and
* returns an `isError` ToolExecutionResult carrying the structured error, so
* the model can self-correct and downstream plugins can route on the code.
*/
export class ToolArgsError extends Error {
/** Machine-routable code; stable across the message wording. */
readonly code = 'INVALID_ARGS'
export class ToolArgsError extends HarnessError {
/** The individual violation messages, in declaration order. */
readonly violations: string[]
constructor(violations: string[]) {
super(`invalid arguments: ${violations.join('; ')}`)
super(`invalid arguments: ${violations.join('; ')}`, 'INVALID_ARGS')
this.name = 'ToolArgsError'
this.violations = violations
}