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

@@ -8,6 +8,7 @@
import { Context, Service } from 'cordis'
import type { ContentBlock, Message, MessageSource } from '@deepseek-ai/dsh-llm'
import { SessionId } from './types.ts'
import type { SessionEvent, SessionEventMap, SessionEventType } from './types.ts'
export * from './types.ts'
@@ -60,7 +61,7 @@ export class Session {
/** Set by the store so appends are observable; undefined when detached. */
onAppend: ((event: SessionEvent) => void) | undefined
constructor(public readonly id: string, seed?: SessionEvent[]) {
constructor(public readonly id: SessionId, seed?: SessionEvent[]) {
if (seed) this.log = [...seed]
}
@@ -155,16 +156,16 @@ export class SessionStore extends Service {
* session from the store.
*/
create(id?: string, seed?: SessionEvent[]): Session {
id ??= `session-${++this.counter}`
if (this.store.has(id)) throw new Error(`session "${id}" already exists`)
const session = new Session(id, seed)
const sessionId = SessionId(id ?? `session-${++this.counter}`)
if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`)
const session = new Session(sessionId, seed)
this.ctx.effect(() => {
session.onAppend = (event) => { this.ctx.emit('session/event', session, event) }
this.store.set(id, session)
this.store.set(sessionId, session)
this.ctx.emit('session/created', session)
return () => {
session.onAppend = undefined
this.store.delete(id)
this.store.delete(sessionId)
}
}, 'sessions.create()')
return session

View File

@@ -1,4 +1,12 @@
import type { ContentBlock, MessageSource, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'
import type { Branded, CallId, ContentBlock, MessageSource, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'
/** Identifies one session in the store (and its persistence artifacts). */
export type SessionId = Branded<'SessionId'>
/** Brand a string as a {@link SessionId}. */
export function SessionId(id: string): SessionId {
return id as SessionId
}
/**
* What started a turn.
@@ -53,8 +61,8 @@ export interface SessionEventMap {
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
/** Assembled assistant message for one step (derived history uses this). */
'assistant/message': { turn: number; step: number; content: ContentBlock[] }
'tool/call': { turn: number; step: number; callId: string; name: string; arguments: string }
'tool/result': { turn: number; step: number; callId: string; content: ContentBlock[]; isError: boolean }
'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }
'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean }
/** Steering content injected between steps of a running turn. */
'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
'usage': { turn: number; step: number; usage: TokenUsage }