Add branded ID types: CallId, SessionId, AgentId

Nominal string types via a unique-symbol brand (zero runtime cost):
an AgentId can no longer be passed where a CallId is expected. Each
core package brands the IDs it owns — CallId in dsh-llm (tool-call
correlation across blocks, chunks, session events, and execution
results), SessionId in dsh-session, AgentId in dsh-agent. Construction
goes through same-named factory functions; public string-in APIs
(sessions.create, agentLoop.create) keep accepting plain strings and
brand internally. Policy note in the brand module: brand IDs that
cross package boundaries, not every string.
This commit is contained in:
Tianyi Cui
2026-06-11 15:17:56 +08:00
parent 86955b96a4
commit 225ed051b1
19 changed files with 135 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { BlockAssembler, type StreamChunk } from '@deepseek-ai/dsh-llm'
import { BlockAssembler, CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
describe('BlockAssembler', () => {
it('assembles interleaved text, reasoning, and tool-call deltas', () => {
@@ -11,8 +11,8 @@ describe('BlockAssembler', () => {
{ type: 'text-delta', index: 1, text: 'Hello' },
{ type: 'text-delta', index: 1, text: ' world' },
{ type: 'block-start', index: 2, blockType: 'tool-call' },
{ type: 'tool-call-delta', index: 2, id: 'call-1', name: 'echo', argumentsDelta: '{"text":' },
{ type: 'tool-call-delta', index: 2, id: 'call-1', argumentsDelta: '"hi"}' },
{ type: 'tool-call-delta', index: 2, id: CallId('call-1'), name: 'echo', argumentsDelta: '{"text":' },
{ type: 'tool-call-delta', index: 2, id: CallId('call-1'), argumentsDelta: '"hi"}' },
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
{ type: 'finish', reason: { kind: 'tool-calls' } },
]
@@ -22,7 +22,7 @@ describe('BlockAssembler', () => {
expect(assembler.blocks()).toEqual([
{ type: 'reasoning', text: 'thinking…' },
{ type: 'text', text: 'Hello world' },
{ type: 'tool-call', id: 'call-1', name: 'echo', arguments: '{"text":"hi"}' },
{ type: 'tool-call', id: CallId('call-1'), name: 'echo', arguments: '{"text":"hi"}' },
])
expect(assembler.usage).toEqual({ inputTokens: 10, outputTokens: 5 })
expect(assembler.finish).toEqual({ kind: 'tool-calls' })
@@ -125,11 +125,11 @@ describe('BlockAssembler', () => {
it('ignores tool-call-delta stragglers after block-end', () => {
const assembler = new BlockAssembler()
assembler.push({ type: 'block-start', index: 0, blockType: 'tool-call' })
assembler.push({ type: 'tool-call-delta', index: 0, id: 'c1', name: 'echo', argumentsDelta: '{}' })
assembler.push({ type: 'block-end', index: 0, block: { type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' } })
assembler.push({ type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'echo', argumentsDelta: '{}' })
assembler.push({ type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' } })
// straggler after block-end — partial.block is set, so early return
assembler.push({ type: 'tool-call-delta', index: 0, id: 'c1', name: 'evil', argumentsDelta: 'oops' })
expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' }])
assembler.push({ type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'evil', argumentsDelta: 'oops' })
expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }])
})
it('assembles tool-call with generated id fallback when no id provided', () => {
@@ -138,7 +138,7 @@ describe('BlockAssembler', () => {
// No id and no name provided — uses fallback id `call-{index}` and empty name
const blocks = assembler.blocks()
expect(blocks).toEqual([
{ type: 'tool-call', id: 'call-0', name: '', arguments: '{}' },
{ type: 'tool-call', id: CallId('call-0'), name: '', arguments: '{}' },
])
})