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

@@ -94,6 +94,28 @@ describe('LlmService', () => {
expect(err.code).toBe('CUSTOM_CODE')
})
it('LlmError extends the shared HarnessError base', async () => {
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
const err = new LlmError('boom', 'AUTH', 401)
expect(err).toBeInstanceOf(HarnessError)
expect(isHarnessError(err)).toBe(true)
expect(err.code).toBe('AUTH')
expect(err.status).toBe(401)
})
it('HarnessError carries a code, names itself by subclass, and chains cause', async () => {
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
const root = new Error('root cause')
const err = new HarnessError('wrapper', 'UNKNOWN', { cause: root })
expect(err).toBeInstanceOf(Error)
expect(err.name).toBe('HarnessError')
expect(err.code).toBe('UNKNOWN')
expect(err.cause).toBe(root)
expect(isHarnessError(err)).toBe(true)
expect(isHarnessError(root)).toBe(false)
expect(isHarnessError('nope')).toBe(false)
})
it('disposes adapter registration on adapter-change event emission', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)