@codeany/open-agent-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/agent.ts CHANGED
@@ -8,6 +8,14 @@
8
8
  * import { createAgent } from 'open-agent-sdk'
9
9
  * const agent = createAgent({ model: 'claude-sonnet-4-6' })
10
10
  * for await (const event of agent.query('Hello')) { ... }
11
+ *
12
+ * // OpenAI-compatible models
13
+ * const agent = createAgent({
14
+ * apiType: 'openai-completions',
15
+ * model: 'gpt-4o',
16
+ * apiKey: 'sk-...',
17
+ * baseURL: 'https://api.openai.com/v1',
18
+ * })
11
19
  */
12
20
 
13
21
  import type {
@@ -17,7 +25,6 @@ import type {
17
25
  ToolDefinition,
18
26
  CanUseToolFn,
19
27
  Message,
20
- TokenUsage,
21
28
  PermissionMode,
22
29
  } from './types.js'
23
30
  import { QueryEngine } from './engine.js'
@@ -29,7 +36,10 @@ import {
29
36
  saveSession,
30
37
  loadSession,
31
38
  } from './session.js'
32
- import type Anthropic from '@anthropic-ai/sdk'
39
+ import { createHookRegistry, type HookRegistry } from './hooks.js'
40
+ import { initBundledSkills } from './skills/index.js'
41
+ import { createProvider, type LLMProvider, type ApiType } from './providers/index.js'
42
+ import type { NormalizedMessageParam } from './providers/types.js'
33
43
 
34
44
  // --------------------------------------------------------------------------
35
45
  // Agent class
@@ -39,14 +49,17 @@ export class Agent {
39
49
  private cfg: AgentOptions
40
50
  private toolPool: ToolDefinition[]
41
51
  private modelId: string
52
+ private apiType: ApiType
42
53
  private apiCredentials: { key?: string; baseUrl?: string }
54
+ private provider: LLMProvider
43
55
  private mcpLinks: MCPConnection[] = []
44
- private history: Anthropic.MessageParam[] = []
56
+ private history: NormalizedMessageParam[] = []
45
57
  private messageLog: Message[] = []
46
58
  private setupDone: Promise<void>
47
59
  private sid: string
48
60
  private abortCtrl: AbortController | null = null
49
61
  private currentEngine: QueryEngine | null = null
62
+ private hookRegistry: HookRegistry
50
63
 
51
64
  constructor(options: AgentOptions = {}) {
52
65
  // Shallow copy to avoid mutating caller's object
@@ -57,13 +70,38 @@ export class Agent {
57
70
  this.modelId = this.cfg.model ?? this.readEnv('CODEANY_MODEL') ?? 'claude-sonnet-4-6'
58
71
  this.sid = this.cfg.sessionId ?? crypto.randomUUID()
59
72
 
60
- // The underlying @anthropic-ai/sdk reads ANTHROPIC_API_KEY from process.env,
61
- // so we bridge our resolved credentials into it.
62
- if (this.apiCredentials.key) {
63
- process.env.ANTHROPIC_API_KEY = this.apiCredentials.key
64
- }
65
- if (this.apiCredentials.baseUrl) {
66
- process.env.ANTHROPIC_BASE_URL = this.apiCredentials.baseUrl
73
+ // Resolve API type
74
+ this.apiType = this.resolveApiType()
75
+
76
+ // Create LLM provider
77
+ this.provider = createProvider(this.apiType, {
78
+ apiKey: this.apiCredentials.key,
79
+ baseURL: this.apiCredentials.baseUrl,
80
+ })
81
+
82
+ // Initialize bundled skills
83
+ initBundledSkills()
84
+
85
+ // Build hook registry from options
86
+ this.hookRegistry = createHookRegistry()
87
+ if (this.cfg.hooks) {
88
+ // Convert AgentOptions hooks format to HookConfig
89
+ for (const [event, defs] of Object.entries(this.cfg.hooks)) {
90
+ for (const def of defs) {
91
+ for (const handler of def.hooks) {
92
+ this.hookRegistry.register(event as any, {
93
+ matcher: def.matcher,
94
+ timeout: def.timeout,
95
+ handler: async (input) => {
96
+ const result = await handler(input, input.toolUseId || '', {
97
+ signal: this.abortCtrl?.signal || new AbortController().signal,
98
+ })
99
+ return result || undefined
100
+ },
101
+ })
102
+ }
103
+ }
104
+ }
67
105
  }
68
106
 
69
107
  // Build tool pool from options (supports ToolDefinition[], string[], or preset)
@@ -73,6 +111,41 @@ export class Agent {
73
111
  this.setupDone = this.setup()
74
112
  }
75
113
 
114
+ /**
115
+ * Resolve API type from options, env, or model name heuristic.
116
+ */
117
+ private resolveApiType(): ApiType {
118
+ // Explicit option
119
+ if (this.cfg.apiType) return this.cfg.apiType
120
+
121
+ // Env var
122
+ const envType =
123
+ this.cfg.env?.CODEANY_API_TYPE ??
124
+ this.readEnv('CODEANY_API_TYPE')
125
+ if (envType === 'openai-completions' || envType === 'anthropic-messages') {
126
+ return envType
127
+ }
128
+
129
+ // Heuristic from model name
130
+ const model = this.modelId.toLowerCase()
131
+ if (
132
+ model.includes('gpt-') ||
133
+ model.includes('o1') ||
134
+ model.includes('o3') ||
135
+ model.includes('o4') ||
136
+ model.includes('deepseek') ||
137
+ model.includes('qwen') ||
138
+ model.includes('yi-') ||
139
+ model.includes('glm') ||
140
+ model.includes('mistral') ||
141
+ model.includes('gemma')
142
+ ) {
143
+ return 'openai-completions'
144
+ }
145
+
146
+ return 'anthropic-messages'
147
+ }
148
+
76
149
  /** Pick API key and base URL from options or CODEANY_* env vars. */
77
150
  private pickCredentials(): { key?: string; baseUrl?: string } {
78
151
  const envMap = this.cfg.env
@@ -208,12 +281,21 @@ export class Agent {
208
281
  }
209
282
  }
210
283
 
284
+ // Recreate provider if overrides change credentials or apiType
285
+ let provider = this.provider
286
+ if (overrides?.apiType || overrides?.apiKey || overrides?.baseURL) {
287
+ const resolvedApiType = overrides.apiType ?? this.apiType
288
+ provider = createProvider(resolvedApiType, {
289
+ apiKey: overrides.apiKey ?? this.apiCredentials.key,
290
+ baseURL: overrides.baseURL ?? this.apiCredentials.baseUrl,
291
+ })
292
+ }
293
+
211
294
  // Create query engine with current conversation state
212
295
  const engine = new QueryEngine({
213
296
  cwd,
214
297
  model: opts.model || this.modelId,
215
- apiKey: this.apiCredentials.key,
216
- baseURL: this.apiCredentials.baseUrl,
298
+ provider,
217
299
  tools,
218
300
  systemPrompt,
219
301
  appendSystemPrompt,
@@ -226,6 +308,8 @@ export class Agent {
226
308
  includePartialMessages: opts.includePartialMessages ?? false,
227
309
  abortSignal: this.abortCtrl.signal,
228
310
  agents: opts.agents,
311
+ hookRegistry: this.hookRegistry,
312
+ sessionId: this.sid,
229
313
  })
230
314
  this.currentEngine = engine
231
315
 
@@ -279,9 +363,9 @@ export class Agent {
279
363
  switch (ev.type) {
280
364
  case 'assistant': {
281
365
  // Extract the last assistant text (multi-turn: only final answer matters)
282
- const fragments = ev.message.content
283
- .filter((c): c is Anthropic.TextBlock => c.type === 'text')
284
- .map((c) => c.text)
366
+ const fragments = (ev.message.content as any[])
367
+ .filter((c: any) => c.type === 'text')
368
+ .map((c: any) => c.text)
285
369
  if (fragments.length) collected.text = fragments.join('')
286
370
  break
287
371
  }
@@ -359,6 +443,13 @@ export class Agent {
359
443
  return this.sid
360
444
  }
361
445
 
446
+ /**
447
+ * Get the current API type.
448
+ */
449
+ getApiType(): ApiType {
450
+ return this.apiType
451
+ }
452
+
362
453
  /**
363
454
  * Stop a background task.
364
455
  */
package/src/engine.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Manages the full conversation lifecycle:
5
5
  * 1. Take user prompt
6
6
  * 2. Build system prompt with context (git status, project context, tools)
7
- * 3. Call LLM API with tools
7
+ * 3. Call LLM API with tools (via provider abstraction)
8
8
  * 4. Stream response
9
9
  * 5. Execute tool calls (concurrent for read-only, serial for mutations)
10
10
  * 6. Send results back, repeat until done
@@ -12,7 +12,6 @@
12
12
  * 8. Retry with exponential backoff on transient errors
13
13
  */
14
14
 
15
- import Anthropic from '@anthropic-ai/sdk'
16
15
  import type {
17
16
  SDKMessage,
18
17
  QueryEngineConfig,
@@ -21,7 +20,12 @@ import type {
21
20
  ToolContext,
22
21
  TokenUsage,
23
22
  } from './types.js'
24
- import { toApiTool } from './tools/types.js'
23
+ import type {
24
+ LLMProvider,
25
+ CreateMessageResponse,
26
+ NormalizedMessageParam,
27
+ NormalizedTool,
28
+ } from './providers/types.js'
25
29
  import {
26
30
  estimateMessagesTokens,
27
31
  estimateCost,
@@ -37,10 +41,34 @@ import {
37
41
  import {
38
42
  withRetry,
39
43
  isPromptTooLongError,
40
- formatApiError,
41
44
  } from './utils/retry.js'
42
45
  import { getSystemContext, getUserContext } from './utils/context.js'
43
46
  import { normalizeMessagesForAPI } from './utils/messages.js'
47
+ import type { HookRegistry, HookInput, HookOutput } from './hooks.js'
48
+
49
+ // ============================================================================
50
+ // Tool format conversion
51
+ // ============================================================================
52
+
53
+ /** Convert a ToolDefinition to the normalized provider tool format. */
54
+ function toProviderTool(tool: ToolDefinition): NormalizedTool {
55
+ return {
56
+ name: tool.name,
57
+ description: tool.description,
58
+ input_schema: tool.inputSchema,
59
+ }
60
+ }
61
+
62
+ // ============================================================================
63
+ // ToolUseBlock (internal type for extracted tool_use blocks)
64
+ // ============================================================================
65
+
66
+ interface ToolUseBlock {
67
+ type: 'tool_use'
68
+ id: string
69
+ name: string
70
+ input: any
71
+ }
44
72
 
45
73
  // ============================================================================
46
74
  // System Prompt Builder
@@ -113,23 +141,43 @@ async function buildSystemPrompt(config: QueryEngineConfig): Promise<string> {
113
141
 
114
142
  export class QueryEngine {
115
143
  private config: QueryEngineConfig
116
- private client: Anthropic
117
- public messages: Anthropic.MessageParam[] = []
144
+ private provider: LLMProvider
145
+ public messages: NormalizedMessageParam[] = []
118
146
  private totalUsage: TokenUsage = { input_tokens: 0, output_tokens: 0 }
119
147
  private totalCost = 0
120
148
  private turnCount = 0
121
149
  private compactState: AutoCompactState
122
150
  private sessionId: string
123
151
  private apiTimeMs = 0
152
+ private hookRegistry?: HookRegistry
124
153
 
125
154
  constructor(config: QueryEngineConfig) {
126
155
  this.config = config
127
- this.client = new Anthropic({
128
- apiKey: config.apiKey,
129
- baseURL: config.baseURL,
130
- })
156
+ this.provider = config.provider
131
157
  this.compactState = createAutoCompactState()
132
- this.sessionId = crypto.randomUUID()
158
+ this.sessionId = config.sessionId || crypto.randomUUID()
159
+ this.hookRegistry = config.hookRegistry
160
+ }
161
+
162
+ /**
163
+ * Execute hooks for a lifecycle event.
164
+ * Returns hook outputs; never throws.
165
+ */
166
+ private async executeHooks(
167
+ event: import('./hooks.js').HookEvent,
168
+ extra?: Partial<HookInput>,
169
+ ): Promise<HookOutput[]> {
170
+ if (!this.hookRegistry?.hasHooks(event)) return []
171
+ try {
172
+ return await this.hookRegistry.execute(event, {
173
+ event,
174
+ sessionId: this.sessionId,
175
+ cwd: this.config.cwd,
176
+ ...extra,
177
+ })
178
+ } catch {
179
+ return []
180
+ }
133
181
  }
134
182
 
135
183
  /**
@@ -137,13 +185,34 @@ export class QueryEngine {
137
185
  * Yields SDKMessage events as the agent works.
138
186
  */
139
187
  async *submitMessage(
140
- prompt: string | Anthropic.ContentBlockParam[],
188
+ prompt: string | any[],
141
189
  ): AsyncGenerator<SDKMessage> {
190
+ // Hook: SessionStart
191
+ await this.executeHooks('SessionStart')
192
+
193
+ // Hook: UserPromptSubmit
194
+ const userHookResults = await this.executeHooks('UserPromptSubmit', {
195
+ toolInput: prompt,
196
+ })
197
+ // Check if any hook blocks the submission
198
+ if (userHookResults.some((r) => r.block)) {
199
+ yield {
200
+ type: 'result',
201
+ subtype: 'error_during_execution',
202
+ is_error: true,
203
+ usage: this.totalUsage,
204
+ num_turns: 0,
205
+ cost: 0,
206
+ errors: ['Blocked by UserPromptSubmit hook'],
207
+ }
208
+ return
209
+ }
210
+
142
211
  // Add user message
143
- this.messages.push({ role: 'user', content: prompt })
212
+ this.messages.push({ role: 'user', content: prompt as any })
144
213
 
145
- // Build tool definitions for API
146
- const tools = this.config.tools.map(toApiTool)
214
+ // Build tool definitions for provider
215
+ const tools = this.config.tools.map(toProviderTool)
147
216
 
148
217
  // Build system prompt
149
218
  const systemPrompt = await buildSystemPrompt(this.config)
@@ -176,16 +245,18 @@ export class QueryEngine {
176
245
  }
177
246
 
178
247
  // Auto-compact if context is too large
179
- if (shouldAutoCompact(this.messages, this.config.model, this.compactState)) {
248
+ if (shouldAutoCompact(this.messages as any[], this.config.model, this.compactState)) {
249
+ await this.executeHooks('PreCompact')
180
250
  try {
181
251
  const result = await compactConversation(
182
- this.client,
252
+ this.provider,
183
253
  this.config.model,
184
- this.messages,
254
+ this.messages as any[],
185
255
  this.compactState,
186
256
  )
187
- this.messages = result.compactedMessages
257
+ this.messages = result.compactedMessages as NormalizedMessageParam[]
188
258
  this.compactState = result.state
259
+ await this.executeHooks('PostCompact')
189
260
  } catch {
190
261
  // Continue with uncompacted messages
191
262
  }
@@ -193,38 +264,33 @@ export class QueryEngine {
193
264
 
194
265
  // Micro-compact: truncate large tool results
195
266
  const apiMessages = microCompactMessages(
196
- normalizeMessagesForAPI(this.messages),
197
- )
267
+ normalizeMessagesForAPI(this.messages as any[]),
268
+ ) as NormalizedMessageParam[]
198
269
 
199
270
  this.turnCount++
200
271
  turnsRemaining--
201
272
 
202
- // Make API call with retry
203
- let response: Anthropic.Message
273
+ // Make API call with retry via provider
274
+ let response: CreateMessageResponse
204
275
  const apiStart = performance.now()
205
276
  try {
206
277
  response = await withRetry(
207
278
  async () => {
208
- const requestParams: Anthropic.MessageCreateParamsNonStreaming = {
279
+ return this.provider.createMessage({
209
280
  model: this.config.model,
210
- max_tokens: this.config.maxTokens,
281
+ maxTokens: this.config.maxTokens,
211
282
  system: systemPrompt,
212
283
  messages: apiMessages,
213
284
  tools: tools.length > 0 ? tools : undefined,
214
- }
215
-
216
- // Add thinking if configured
217
- if (
218
- this.config.thinking?.type === 'enabled' &&
219
- this.config.thinking.budgetTokens
220
- ) {
221
- (requestParams as any).thinking = {
222
- type: 'enabled',
223
- budget_tokens: this.config.thinking.budgetTokens,
224
- }
225
- }
226
-
227
- return this.client.messages.create(requestParams)
285
+ thinking:
286
+ this.config.thinking?.type === 'enabled' &&
287
+ this.config.thinking.budgetTokens
288
+ ? {
289
+ type: 'enabled',
290
+ budget_tokens: this.config.thinking.budgetTokens,
291
+ }
292
+ : undefined,
293
+ })
228
294
  },
229
295
  undefined,
230
296
  this.config.abortSignal,
@@ -234,12 +300,12 @@ export class QueryEngine {
234
300
  if (isPromptTooLongError(err) && !this.compactState.compacted) {
235
301
  try {
236
302
  const result = await compactConversation(
237
- this.client,
303
+ this.provider,
238
304
  this.config.model,
239
- this.messages,
305
+ this.messages as any[],
240
306
  this.compactState,
241
307
  )
242
- this.messages = result.compactedMessages
308
+ this.messages = result.compactedMessages as NormalizedMessageParam[]
243
309
  this.compactState = result.state
244
310
  turnsRemaining++ // Retry this turn
245
311
  this.turnCount--
@@ -262,38 +328,38 @@ export class QueryEngine {
262
328
  // Track API timing
263
329
  this.apiTimeMs += performance.now() - apiStart
264
330
 
265
- // Track usage
331
+ // Track usage (normalized by provider)
266
332
  if (response.usage) {
267
333
  this.totalUsage.input_tokens += response.usage.input_tokens
268
334
  this.totalUsage.output_tokens += response.usage.output_tokens
269
- if ('cache_creation_input_tokens' in response.usage) {
335
+ if (response.usage.cache_creation_input_tokens) {
270
336
  this.totalUsage.cache_creation_input_tokens =
271
337
  (this.totalUsage.cache_creation_input_tokens || 0) +
272
- ((response.usage as any).cache_creation_input_tokens || 0)
338
+ response.usage.cache_creation_input_tokens
273
339
  }
274
- if ('cache_read_input_tokens' in response.usage) {
340
+ if (response.usage.cache_read_input_tokens) {
275
341
  this.totalUsage.cache_read_input_tokens =
276
342
  (this.totalUsage.cache_read_input_tokens || 0) +
277
- ((response.usage as any).cache_read_input_tokens || 0)
343
+ response.usage.cache_read_input_tokens
278
344
  }
279
- this.totalCost += estimateCost(this.config.model, response.usage as TokenUsage)
345
+ this.totalCost += estimateCost(this.config.model, response.usage)
280
346
  }
281
347
 
282
348
  // Add assistant message to conversation
283
- this.messages.push({ role: 'assistant', content: response.content })
349
+ this.messages.push({ role: 'assistant', content: response.content as any })
284
350
 
285
351
  // Yield assistant message
286
352
  yield {
287
353
  type: 'assistant',
288
354
  message: {
289
355
  role: 'assistant',
290
- content: response.content,
356
+ content: response.content as any,
291
357
  },
292
358
  }
293
359
 
294
360
  // Handle max_output_tokens recovery
295
361
  if (
296
- response.stop_reason === 'max_tokens' &&
362
+ response.stopReason === 'max_tokens' &&
297
363
  maxOutputRecoveryAttempts < MAX_OUTPUT_RECOVERY
298
364
  ) {
299
365
  maxOutputRecoveryAttempts++
@@ -307,7 +373,7 @@ export class QueryEngine {
307
373
 
308
374
  // Check for tool use
309
375
  const toolUseBlocks = response.content.filter(
310
- (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use',
376
+ (block): block is ToolUseBlock => block.type === 'tool_use',
311
377
  )
312
378
 
313
379
  if (toolUseBlocks.length === 0) {
@@ -349,9 +415,15 @@ export class QueryEngine {
349
415
  })),
350
416
  })
351
417
 
352
- if (response.stop_reason === 'end_turn') break
418
+ if (response.stopReason === 'end_turn') break
353
419
  }
354
420
 
421
+ // Hook: Stop (end of agentic loop)
422
+ await this.executeHooks('Stop')
423
+
424
+ // Hook: SessionEnd
425
+ await this.executeHooks('SessionEnd')
426
+
355
427
  // Yield enriched final result
356
428
  const endSubtype = budgetExceeded
357
429
  ? 'error_max_budget_usd'
@@ -380,11 +452,14 @@ export class QueryEngine {
380
452
  * Mutation tools run sequentially.
381
453
  */
382
454
  private async executeTools(
383
- toolUseBlocks: Anthropic.ToolUseBlock[],
455
+ toolUseBlocks: ToolUseBlock[],
384
456
  ): Promise<(ToolResult & { tool_name?: string })[]> {
385
457
  const context: ToolContext = {
386
458
  cwd: this.config.cwd,
387
459
  abortSignal: this.config.abortSignal,
460
+ provider: this.provider,
461
+ model: this.config.model,
462
+ apiType: this.provider.apiType,
388
463
  }
389
464
 
390
465
  const MAX_CONCURRENCY = parseInt(
@@ -392,8 +467,8 @@ export class QueryEngine {
392
467
  )
393
468
 
394
469
  // Partition into read-only (concurrent) and mutation (serial)
395
- const readOnly: Array<{ block: Anthropic.ToolUseBlock; tool?: ToolDefinition }> = []
396
- const mutations: Array<{ block: Anthropic.ToolUseBlock; tool?: ToolDefinition }> = []
470
+ const readOnly: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
471
+ const mutations: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
397
472
 
398
473
  for (const block of toolUseBlocks) {
399
474
  const tool = this.config.tools.find((t) => t.name === block.name)
@@ -430,7 +505,7 @@ export class QueryEngine {
430
505
  * Execute a single tool with permission checking.
431
506
  */
432
507
  private async executeSingleTool(
433
- block: Anthropic.ToolUseBlock,
508
+ block: ToolUseBlock,
434
509
  tool: ToolDefinition | undefined,
435
510
  context: ToolContext,
436
511
  ): Promise<ToolResult & { tool_name?: string }> {
@@ -469,7 +544,7 @@ export class QueryEngine {
469
544
  }
470
545
  }
471
546
  if (permission.updatedInput !== undefined) {
472
- block = { ...block, input: permission.updatedInput as Record<string, unknown> }
547
+ block = { ...block, input: permission.updatedInput }
473
548
  }
474
549
  } catch (err: any) {
475
550
  return {
@@ -482,11 +557,46 @@ export class QueryEngine {
482
557
  }
483
558
  }
484
559
 
560
+ // Hook: PreToolUse
561
+ const preHookResults = await this.executeHooks('PreToolUse', {
562
+ toolName: block.name,
563
+ toolInput: block.input,
564
+ toolUseId: block.id,
565
+ })
566
+ // Check if any hook blocks this tool
567
+ if (preHookResults.some((r) => r.block)) {
568
+ const msg = preHookResults.find((r) => r.message)?.message || 'Blocked by PreToolUse hook'
569
+ return {
570
+ type: 'tool_result',
571
+ tool_use_id: block.id,
572
+ content: msg,
573
+ is_error: true,
574
+ tool_name: block.name,
575
+ }
576
+ }
577
+
485
578
  // Execute the tool
486
579
  try {
487
580
  const result = await tool.call(block.input, context)
581
+
582
+ // Hook: PostToolUse
583
+ await this.executeHooks('PostToolUse', {
584
+ toolName: block.name,
585
+ toolInput: block.input,
586
+ toolOutput: typeof result.content === 'string' ? result.content : JSON.stringify(result.content),
587
+ toolUseId: block.id,
588
+ })
589
+
488
590
  return { ...result, tool_use_id: block.id, tool_name: block.name }
489
591
  } catch (err: any) {
592
+ // Hook: PostToolUseFailure
593
+ await this.executeHooks('PostToolUseFailure', {
594
+ toolName: block.name,
595
+ toolInput: block.input,
596
+ toolUseId: block.id,
597
+ error: err.message,
598
+ })
599
+
490
600
  return {
491
601
  type: 'tool_result',
492
602
  tool_use_id: block.id,
@@ -500,7 +610,7 @@ export class QueryEngine {
500
610
  /**
501
611
  * Get current messages for session persistence.
502
612
  */
503
- getMessages(): Anthropic.MessageParam[] {
613
+ getMessages(): NormalizedMessageParam[] {
504
614
  return [...this.messages]
505
615
  }
506
616