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

@@ -5,12 +5,13 @@
* @module @deepseek-ai/dsh-llm/assembler
*/
import { CallId } from './brand.ts'
import type { ContentBlock, FinishReason, GenerateResult, Message, StreamChunk, TokenUsage } from './types.ts'
interface PartialBlock {
blockType: string
text: string
toolCallId?: string
toolCallId?: CallId
toolCallName?: string
toolCallArguments: string
/** Set by `block-end` — authoritative, and freezes the partial. */
@@ -101,7 +102,7 @@ export class BlockAssembler {
case 'reasoning': return { type: 'reasoning', text: partial.text }
case 'tool-call': return {
type: 'tool-call',
id: partial.toolCallId ?? `call-${index}`,
id: partial.toolCallId ?? CallId(`call-${index}`),
name: partial.toolCallName ?? '',
arguments: partial.toolCallArguments,
}

32
packages/llm/src/brand.ts Normal file
View File

@@ -0,0 +1,32 @@
/**
* Branded (nominal) ID types.
*
* A brand makes structurally-identical strings non-interchangeable at the
* type level: an `AgentId` cannot be passed where a `CallId` is expected,
* even though both are strings at runtime. Construction goes through the
* per-type factory (a plain cast inside — zero runtime cost); comparison,
* logging, and serialization all behave as ordinary strings.
*
* Policy: core packages brand the IDs they own — `CallId` here (tool-call
* correlation), `SessionId` in dsh-session, `AgentId` in dsh-agent. Branding
* is for IDs that cross package boundaries and could plausibly be confused;
* not every string needs a brand.
*
* @module @deepseek-ai/dsh-llm/brand
*/
declare const BRAND: unique symbol
/** A string carrying a compile-time-only brand `B`. */
export type Branded<B extends string> = string & { readonly [BRAND]: B }
/**
* Correlates a model-issued tool call with its result. Provider-issued for
* real adapters; synthesized by mocks/assembler fallbacks.
*/
export type CallId = Branded<'CallId'>
/** Brand a string as a {@link CallId}. */
export function CallId(id: string): CallId {
return id as CallId
}

View File

@@ -10,6 +10,7 @@ import { Context, Service } from 'cordis'
import type { ContentBlock, GenerateOptions, GenerateResult, StreamChunk } from './types.ts'
import { BlockAssembler } from './assembler.ts'
export * from './brand.ts'
export * from './types.ts'
export { BlockAssembler } from './assembler.ts'

View File

@@ -19,6 +19,8 @@
* ```
*/
import type { CallId } from './brand.ts'
/** Cache hint attached to a content block (provider-interpreted). */
export type CacheHint = 'ephemeral'
@@ -39,7 +41,7 @@ export interface ReasoningBlock {
export interface ToolCallBlock {
type: 'tool-call'
/** Provider-issued call id; correlates with the matching tool result. */
id: string
id: CallId
name: string
/** Raw JSON string as produced by the model. */
arguments: string
@@ -48,7 +50,7 @@ export interface ToolCallBlock {
/** The result of a tool invocation, sent back to the model. */
export interface ToolResultBlock {
type: 'tool-result'
toolCallId: string
toolCallId: CallId
content: ContentBlock[]
isError?: boolean
cache?: CacheHint
@@ -134,7 +136,7 @@ export type StreamChunk =
| { type: 'block-start'; index: number; blockType: ContentBlockType }
| { type: 'text-delta'; index: number; text: string }
| { type: 'reasoning-delta'; index: number; text: string }
| { type: 'tool-call-delta'; index: number; id: string; name?: string; argumentsDelta: string }
| { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string }
| { type: 'block-end'; index: number; block: ContentBlock }
| { type: 'usage'; usage: TokenUsage }
| { type: 'finish'; reason: FinishReason }