forked from deepseek-ai/deepseek-harness
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmessage.ts
More file actions
261 lines (238 loc) · 9.23 KB
/
Copy pathmessage.ts
File metadata and controls
261 lines (238 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/** Message value types, identity, and immutable construction helpers. */
import { MessageId, type CallId } from './brand.ts'
import { deepFreeze } from './call-config.ts'
import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts'
/** Provider/model identity and adapter-private replay data for an assistant message. */
export interface AssistantProvenance {
/** Provider route that produced the message. */
provider: string
/** Provider model id that produced the message. */
model: string
/**
* Lossless-JSON adapter state needed to replay the provider response.
* `LlmRuntime` exposes it to a target adapter only when that adapter instance
* currently owns both this historical provider and the target provider.
*/
replayState?: unknown
}
/** Required source of an assistant message produced by a routed model. */
export interface ModelMessageSource extends AssistantProvenance {
kind: 'model'
}
/** Required source of a user-role message carrying one tool result. */
export interface ToolMessageSource {
kind: 'tool'
callId: CallId
}
/**
* The kind of information in producer-supplied context, declared by the
* producer beside its provenance.
*
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
* of thing it is*, and the two axes are deliberately independent — several
* producers share one form, and one producer may emit more than one form over
* a session.
*
* The vocabulary is SEMANTIC, never visual: a value states that the content is
* a file's instructions or a catalog of available items, and a consumer decides
* what that looks like. Colors, icons, ordering, and collapse defaults are the
* consumer's business and must not enter this union. It grows one value at a
* time as producers gain the structured fields their form needs; an absent or
* unknown value is the documented default, presented as opaque content.
*/
export type ContextForm =
/** Instructions read out of workspace files the model is expected to follow. */
| 'instructions'
/** A catalog of items available in this session, republished as it changes. */
| 'catalog'
/** Current state, where a later snapshot from the same producer supersedes an earlier one. */
| 'snapshot'
/** A one-off account of something that just happened; it supersedes nothing. */
| 'notice'
/** A message another agent addressed to this one. */
| 'relay'
/** Material lifted out of another session's log, possibly reduced on the way in. */
| 'recall'
/** One named contribution to a `snapshot`-form context, in assembly order. */
export interface ContextSnapshotSection {
/** The contributing subsystem's name. */
readonly name: string
/** That contribution's model-facing text, exactly as assembled. */
readonly text: string
}
/**
* Producer-declared {@link ContextForm} and the fields that form requires,
* mixed into the source types that carry one.
*
* Discriminated by `form` so a producer cannot select a form without the
* fields needed to present it: a `notice` must record its one-line
* account, a `snapshot` its sections. Omitting `form` stays valid — an
* undeclared context is the documented default.
*/
export type ContextFormed =
| { readonly form?: never }
| { readonly form: 'instructions' }
| { readonly form: 'catalog' }
| {
readonly form: 'snapshot'
/** The named contributions this snapshot assembled, in order. */
readonly sections: readonly ContextSnapshotSection[]
}
| {
readonly form: 'notice'
/** One-line account of what happened, shown without expanding the row. */
readonly summary: string
}
| { readonly form: 'relay' }
| { readonly form: 'recall' }
/**
* Where a message (or injected content) came from.
* Merge-extensible sum type — plugins add their own `kind`s.
*/
export interface MessageSourceMap {
user: { kind: 'user' }
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
model: ModelMessageSource
tool: ToolMessageSource
}
/**
* Bound for a `notice` summary. The account rides a collapsed transcript row
* and is committed to the durable log, while its inputs — task labels, goal
* objectives, tool arguments — are caller text with no length of their own.
*/
export const CONTEXT_SUMMARY_MAX_CHARS = 120
/**
* Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
* @param summary - the producer's one-line account, of any length.
* @returns the account, ellipsized when it exceeds the bound.
*/
export function boundContextSummary(summary: string): string {
return summary.length <= CONTEXT_SUMMARY_MAX_CHARS
? summary
: `${summary.slice(0, CONTEXT_SUMMARY_MAX_CHARS - 1)}…`
}
/** Any known message source, derived from {@link MessageSourceMap}; switch on `kind` and fall through unknowns (merge-extensible). */
export type MessageSource = MessageSourceMap[keyof MessageSourceMap]
/** One immutable message representation shared by delivery, durable history, and model requests. */
export interface Message {
/** Stable identity preserved across every representation boundary. */
readonly id: MessageId
/** Provider-neutral conversation role. */
readonly role: 'system' | 'user' | 'assistant'
/** Exact model-facing blocks. */
readonly content: ContentBlock[]
/** Required source fields supplied by the producer. */
readonly source: MessageSource
}
/** A user-role specialization of the one shared message representation. */
export interface UserMessage extends Message {
readonly role: 'user'
}
/** A model-produced assistant specialization of the shared message representation. */
export interface AssistantMessage extends Message {
readonly role: 'assistant'
readonly source: ModelMessageSource
}
/** A tool-result specialization whose model-facing block retains call correlation. */
export interface ToolResultMessage extends Message {
readonly role: 'user'
readonly content: [ToolResultBlock]
readonly source: ToolMessageSource
}
type NewMessage = Omit<Message, 'id'>
type NewUserMessage = Omit<UserMessage, 'id' | 'role'>
type NewAssistantMessage = Omit<AssistantMessage, 'id' | 'role' | 'source'> & {
readonly source: Omit<ModelMessageSource, 'kind'> & { readonly kind?: never }
}
/**
* Detach and deep-freeze a message whose identity already exists.
* @param message - complete message, including its stable identity.
* @returns an immutable snapshot that preserves the identity.
*/
export function freezeMessage<T extends Message>(message: T): T {
return deepFreeze(structuredClone(message))
}
/**
* Create one identified message and freeze it before publication.
* @param input - complete role, content, and source for a new message.
* @returns an immutable message with a fresh stable identity.
*/
export function createMessage<T extends NewMessage>(
input: T & { readonly id?: never },
): T & Pick<Message, 'id'> {
return freezeMessage({
...input,
id: MessageId(crypto.randomUUID()),
})
}
/**
* Create one identified user-role message and freeze it before publication.
* @param input - complete content and source for a new user message.
* @returns an immutable user message with a fresh stable identity.
*/
export function createUserMessage<T extends NewUserMessage>(
input: T & { readonly id?: never; readonly role?: never },
): T & Pick<UserMessage, 'id' | 'role'> {
return createMessage({
...input,
role: 'user',
})
}
/**
* Create one identified model-produced assistant message and freeze it before publication.
* @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
* @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
*/
export function createAssistantMessage(
input: NewAssistantMessage & { readonly id?: never; readonly role?: never },
): AssistantMessage {
return createMessage({
role: 'assistant',
content: input.content,
source: {
kind: 'model',
...input.source,
},
})
}
/** Input whose acceptance creates one tool-result message. */
export interface ToolResultMessageInput {
readonly callId: CallId
readonly content: ContentBlock[]
readonly isError: boolean
}
/**
* Create and freeze one identified tool-result message.
* @param input - call identity, raw result blocks, and outcome.
* @returns an immutable user-role tool-result message.
*/
export function createToolResultMessage(input: ToolResultMessageInput): ToolResultMessage {
return createUserMessage({
source: { kind: 'tool', callId: input.callId },
content: [{
type: 'tool-result',
toolCallId: input.callId,
content: input.content,
isError: input.isError,
}],
})
}
/**
* Whether a stream chunk carries visible model output (the first-token
* boundary shared by client step timing and the whole-log sessionStats
* projection). Empty deltas (heartbeats, empty tool-call frames) do not count
* as a first token.
* @param chunk - the stream chunk to test.
* @returns true when the chunk contains a non-empty text/reasoning/tool delta.
*/
export function isTokenDelta(chunk: StreamChunk): boolean {
switch (chunk.type) {
case 'text-delta':
case 'reasoning-delta':
return chunk.text !== ''
case 'tool-call-delta':
return chunk.argumentsDelta !== '' || chunk.name !== undefined
default:
return false
}
}