zeitlich 0.2.0 → 0.2.2

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.
@@ -114,7 +114,6 @@ export const createSession = async <T extends ToolMap>({
114
114
  metadata,
115
115
  });
116
116
  }
117
-
118
117
  stateManager.setTools(toolRouter.getToolDefinitions());
119
118
 
120
119
  await initializeThread(threadId);
@@ -158,29 +157,56 @@ export const createSession = async <T extends ToolMap>({
158
157
  }
159
158
 
160
159
  const rawToolCalls: RawToolCall[] = await parseToolCalls(message);
161
- const parsedToolCalls = rawToolCalls
162
- .filter((tc: RawToolCall) => tc.name !== "Task")
163
- .map((tc: RawToolCall) => toolRouter.parseToolCall(tc));
164
- const taskToolCalls =
165
- subagents && subagents.length > 0
166
- ? rawToolCalls
167
- .filter((tc: RawToolCall) => tc.name === "Task")
168
- .map((tc: RawToolCall) => {
169
- // Parse and validate args using the tool's schema
170
- const parsedArgs = createTaskTool(subagents).schema.parse(
171
- tc.args
172
- );
173
-
174
- return {
175
- id: tc.id ?? "",
176
- name: tc.name,
177
- args: parsedArgs,
178
- } as ParsedToolCall<
179
- "Task",
180
- TaskToolSchemaType<SubagentConfig[]>
181
- >;
182
- })
183
- : [];
160
+
161
+ // Parse tool calls, catching schema errors and returning them to the agent
162
+ const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
163
+ for (const tc of rawToolCalls.filter(
164
+ (tc: RawToolCall) => tc.name !== "Task"
165
+ )) {
166
+ try {
167
+ parsedToolCalls.push(toolRouter.parseToolCall(tc));
168
+ } catch (error) {
169
+ await appendToolResult({
170
+ threadId,
171
+ toolCallId: tc.id ?? "",
172
+ content: JSON.stringify({
173
+ error: `Invalid tool call for "${tc.name}": ${error instanceof Error ? error.message : String(error)}`,
174
+ }),
175
+ });
176
+ }
177
+ }
178
+
179
+ const taskToolCalls: ParsedToolCall<
180
+ "Task",
181
+ TaskToolSchemaType<SubagentConfig[]>
182
+ >[] = [];
183
+ if (subagents && subagents.length > 0) {
184
+ for (const tc of rawToolCalls.filter(
185
+ (tc: RawToolCall) => tc.name === "Task"
186
+ )) {
187
+ try {
188
+ const parsedArgs = createTaskTool(subagents).schema.parse(
189
+ tc.args
190
+ );
191
+ taskToolCalls.push({
192
+ id: tc.id ?? "",
193
+ name: tc.name,
194
+ args: parsedArgs,
195
+ } as ParsedToolCall<
196
+ "Task",
197
+ TaskToolSchemaType<SubagentConfig[]>
198
+ >);
199
+ } catch (error) {
200
+ await appendToolResult({
201
+ threadId,
202
+ toolCallId: tc.id ?? "",
203
+ content: JSON.stringify({
204
+ error: `Invalid tool call for "Task": ${error instanceof Error ? error.message : String(error)}`,
205
+ }),
206
+ });
207
+ }
208
+ }
209
+ }
184
210
 
185
211
  // Hooks can call stateManager.waitForInput() to pause the session
186
212
  await toolRouter.processToolCalls(
@@ -6,6 +6,7 @@ import {
6
6
  isTerminalStatus,
7
7
  } from "./types";
8
8
  import type { ToolDefinition } from "./tool-router";
9
+ import { z } from "zod";
9
10
 
10
11
  /**
11
12
  * JSON primitive types that Temporal can serialize
@@ -105,7 +106,7 @@ export interface AgentStateManager<TCustom extends JsonSerializable<TCustom>> {
105
106
  /** Delete a task by ID */
106
107
  deleteTask(id: string): boolean;
107
108
 
108
- /** Set the tools */
109
+ /** Set the tools (converts Zod schemas to JSON Schema for serialization) */
109
110
  setTools(newTools: ToolDefinition[]): void;
110
111
  }
111
112
 
@@ -245,7 +246,13 @@ export function createAgentStateManager<
245
246
  },
246
247
 
247
248
  setTools(newTools: ToolDefinition[]): void {
248
- tools = newTools;
249
+ tools = newTools.map((tool) => ({
250
+ name: tool.name,
251
+ description: tool.description,
252
+ schema: z.toJSONSchema(tool.schema) as Record<string, unknown>,
253
+ strict: tool.strict,
254
+ max_uses: tool.max_uses,
255
+ }));
249
256
  },
250
257
 
251
258
  deleteTask(id: string): boolean {
@@ -1,6 +1,15 @@
1
1
  import type { MessageToolDefinition } from "@langchain/core/messages";
2
2
  import type { ToolMessageContent } from "./thread-manager";
3
- import type { Hooks, SubagentConfig, ToolResultConfig } from "./types";
3
+ import type {
4
+ Hooks,
5
+ PostToolUseFailureHookResult,
6
+ PreToolUseHookResult,
7
+ SubagentConfig,
8
+ SubagentHooks,
9
+ ToolHooks,
10
+ ToolResultConfig,
11
+ } from "./types";
12
+ import type { GenericTaskToolSchemaType } from "../tools/task/tool";
4
13
 
5
14
  import type { z } from "zod";
6
15
  import { proxyActivities } from "@temporalio/workflow";
@@ -49,6 +58,8 @@ export interface ToolWithHandler<
49
58
  handler: ToolHandler<z.infer<TSchema>, TResult, TContext>;
50
59
  strict?: boolean;
51
60
  max_uses?: number;
61
+ /** Per-tool lifecycle hooks (run in addition to global hooks) */
62
+ hooks?: ToolHooks<z.infer<TSchema>, TResult>;
52
63
  }
53
64
 
54
65
  /**
@@ -68,6 +79,9 @@ export type ToolMap = Record<
68
79
  /* eslint-enable @typescript-eslint/no-explicit-any */
69
80
  strict?: boolean;
70
81
  max_uses?: number;
82
+ /* eslint-disable @typescript-eslint/no-explicit-any */
83
+ hooks?: ToolHooks<any, any>;
84
+ /* eslint-enable @typescript-eslint/no-explicit-any */
71
85
  }
72
86
  >;
73
87
 
@@ -128,10 +142,10 @@ export type AppendToolResultFn = (config: ToolResultConfig) => Promise<void>;
128
142
  * Contains the content for the tool message and the result to return from processToolCalls.
129
143
  */
130
144
  export interface ToolHandlerResponse<TResult> {
131
- /** Content for the tool message added to the thread */
132
- content: ToolMessageContent;
133
- /** Result returned from processToolCalls */
134
- result: TResult;
145
+ /** Content sent back to the LLM as the tool call response */
146
+ toolResponse: ToolMessageContent;
147
+ /** Data returned to the workflow and hooks for further processing */
148
+ data: TResult | null;
135
149
  }
136
150
 
137
151
  /**
@@ -225,7 +239,7 @@ export interface ToolCallResult<
225
239
  > {
226
240
  toolCallId: string;
227
241
  name: TName;
228
- result: TResult;
242
+ data: TResult | null;
229
243
  }
230
244
 
231
245
  /**
@@ -424,9 +438,36 @@ export function createToolRouter<T extends ToolMap>(
424
438
  }
425
439
 
426
440
  if (options.subagents) {
441
+ // Build per-subagent hook dispatcher keyed by subagent name
442
+ const subagentHooksMap = new Map<string, SubagentHooks>();
443
+ for (const s of options.subagents) {
444
+ if (s.hooks) subagentHooksMap.set(s.name, s.hooks);
445
+ }
446
+
447
+ const resolveSubagentName = (args: unknown): string =>
448
+ (args as GenericTaskToolSchemaType).subagent;
449
+
427
450
  toolMap.set("Task", {
428
451
  ...createTaskTool(options.subagents),
429
452
  handler: createTaskHandler(options.subagents),
453
+ ...(subagentHooksMap.size > 0 && {
454
+ hooks: {
455
+ onPreToolUse: async (ctx): Promise<PreToolUseHookResult> => {
456
+ const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
457
+ return hooks?.onPreExecution?.(ctx) ?? {};
458
+ },
459
+ onPostToolUse: async (ctx): Promise<void> => {
460
+ const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
461
+ await hooks?.onPostExecution?.(ctx);
462
+ },
463
+ onPostToolUseFailure: async (
464
+ ctx
465
+ ): Promise<PostToolUseFailureHookResult> => {
466
+ const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
467
+ return hooks?.onExecutionFailure?.(ctx) ?? {};
468
+ },
469
+ } satisfies ToolHooks,
470
+ }),
430
471
  });
431
472
  }
432
473
 
@@ -455,8 +496,10 @@ export function createToolRouter<T extends ToolMap>(
455
496
  handlerContext?: ToolHandlerContext
456
497
  ): Promise<ToolCallResultUnion<TResults> | null> {
457
498
  const startTime = Date.now();
499
+ const tool = toolMap.get(toolCall.name);
500
+ const toolHooks = tool?.hooks;
458
501
 
459
- // PreToolUse hook - can skip or modify args
502
+ // --- PreToolUse: global then per-tool ---
460
503
  let effectiveArgs: unknown = toolCall.args;
461
504
  if (options.hooks?.onPreToolUse) {
462
505
  const preResult = await options.hooks.onPreToolUse({
@@ -465,7 +508,6 @@ export function createToolRouter<T extends ToolMap>(
465
508
  turn,
466
509
  });
467
510
  if (preResult?.skip) {
468
- // Skip this tool call - append a skip message and return null
469
511
  await appendToolResult({
470
512
  threadId: options.threadId,
471
513
  toolCallId: toolCall.id,
@@ -480,10 +522,31 @@ export function createToolRouter<T extends ToolMap>(
480
522
  effectiveArgs = preResult.modifiedArgs;
481
523
  }
482
524
  }
525
+ if (toolHooks?.onPreToolUse) {
526
+ const preResult = await toolHooks.onPreToolUse({
527
+ args: effectiveArgs,
528
+ threadId: options.threadId,
529
+ turn,
530
+ });
531
+ if (preResult?.skip) {
532
+ await appendToolResult({
533
+ threadId: options.threadId,
534
+ toolCallId: toolCall.id,
535
+ content: JSON.stringify({
536
+ skipped: true,
537
+ reason: "Skipped by tool PreToolUse hook",
538
+ }),
539
+ });
540
+ return null;
541
+ }
542
+ if (preResult?.modifiedArgs !== undefined) {
543
+ effectiveArgs = preResult.modifiedArgs;
544
+ }
545
+ }
483
546
 
484
- const tool = toolMap.get(toolCall.name);
547
+ // --- Execute handler ---
485
548
  let result: unknown;
486
- let content: ToolMessageContent;
549
+ let content!: ToolMessageContent;
487
550
 
488
551
  try {
489
552
  if (tool) {
@@ -491,31 +554,54 @@ export function createToolRouter<T extends ToolMap>(
491
554
  effectiveArgs as Parameters<typeof tool.handler>[0],
492
555
  (handlerContext ?? {}) as Parameters<typeof tool.handler>[1]
493
556
  );
494
- result = response.result;
495
- content = response.content;
557
+ result = response.data;
558
+ content = response.toolResponse;
496
559
  } else {
497
560
  result = { error: `Unknown tool: ${toolCall.name}` };
498
561
  content = JSON.stringify(result, null, 2);
499
562
  }
500
563
  } catch (error) {
501
- // PostToolUseFailure hook - can recover from errors
502
- if (options.hooks?.onPostToolUseFailure) {
564
+ // --- PostToolUseFailure: per-tool then global ---
565
+ const err = error instanceof Error ? error : new Error(String(error));
566
+ let recovered = false;
567
+
568
+ if (toolHooks?.onPostToolUseFailure) {
569
+ const failureResult = await toolHooks.onPostToolUseFailure({
570
+ args: effectiveArgs,
571
+ error: err,
572
+ threadId: options.threadId,
573
+ turn,
574
+ });
575
+ if (failureResult?.fallbackContent !== undefined) {
576
+ content = failureResult.fallbackContent;
577
+ result = { error: String(error), recovered: true };
578
+ recovered = true;
579
+ } else if (failureResult?.suppress) {
580
+ content = JSON.stringify({ error: String(error), suppressed: true });
581
+ result = { error: String(error), suppressed: true };
582
+ recovered = true;
583
+ }
584
+ }
585
+
586
+ if (!recovered && options.hooks?.onPostToolUseFailure) {
503
587
  const failureResult = await options.hooks.onPostToolUseFailure({
504
588
  toolCall,
505
- error: error instanceof Error ? error : new Error(String(error)),
589
+ error: err,
506
590
  threadId: options.threadId,
507
591
  turn,
508
592
  });
509
593
  if (failureResult?.fallbackContent !== undefined) {
510
594
  content = failureResult.fallbackContent;
511
595
  result = { error: String(error), recovered: true };
596
+ recovered = true;
512
597
  } else if (failureResult?.suppress) {
513
598
  content = JSON.stringify({ error: String(error), suppressed: true });
514
599
  result = { error: String(error), suppressed: true };
515
- } else {
516
- throw error;
600
+ recovered = true;
517
601
  }
518
- } else {
602
+ }
603
+
604
+ if (!recovered) {
519
605
  throw error;
520
606
  }
521
607
  }
@@ -530,12 +616,21 @@ export function createToolRouter<T extends ToolMap>(
530
616
  const toolResult = {
531
617
  toolCallId: toolCall.id,
532
618
  name: toolCall.name,
533
- result,
619
+ data: result,
534
620
  } as ToolCallResultUnion<TResults>;
535
621
 
536
- // PostToolUse hook - called after successful execution
622
+ // --- PostToolUse: per-tool then global ---
623
+ const durationMs = Date.now() - startTime;
624
+ if (toolHooks?.onPostToolUse) {
625
+ await toolHooks.onPostToolUse({
626
+ args: effectiveArgs,
627
+ result: result,
628
+ threadId: options.threadId,
629
+ turn,
630
+ durationMs,
631
+ });
632
+ }
537
633
  if (options.hooks?.onPostToolUse) {
538
- const durationMs = Date.now() - startTime;
539
634
  await options.hooks.onPostToolUse({
540
635
  toolCall,
541
636
  result: toolResult,
@@ -654,13 +749,13 @@ export function createToolRouter<T extends ToolMap>(
654
749
  await appendToolResult({
655
750
  threadId: options.threadId,
656
751
  toolCallId: toolCall.id,
657
- content: response.content,
752
+ content: response.toolResponse,
658
753
  });
659
754
 
660
755
  return {
661
756
  toolCallId: toolCall.id,
662
757
  name: toolCall.name as TName,
663
- result: response.result,
758
+ data: response.data ?? null,
664
759
  };
665
760
  };
666
761
 
@@ -706,6 +801,93 @@ export function createToolRouter<T extends ToolMap>(
706
801
  };
707
802
  }
708
803
 
804
+ /**
805
+ * Identity function that creates a generic inference context for a tool definition.
806
+ * TypeScript infers TResult from the handler and flows it to hooks automatically.
807
+ *
808
+ * @example
809
+ * ```typescript
810
+ * tools: {
811
+ * AskUser: defineTool({
812
+ * ...askUserTool,
813
+ * handler: handleAskUser,
814
+ * hooks: {
815
+ * onPostToolUse: ({ result }) => {
816
+ * // result is correctly typed as the handler's return data type
817
+ * },
818
+ * },
819
+ * }),
820
+ * }
821
+ * ```
822
+ */
823
+ export function defineTool<
824
+ TName extends string,
825
+ TSchema extends z.ZodType,
826
+ TResult,
827
+ TContext = ToolHandlerContext,
828
+ >(
829
+ tool: ToolWithHandler<TName, TSchema, TResult, TContext>
830
+ ): ToolWithHandler<TName, TSchema, TResult, TContext> {
831
+ return tool;
832
+ }
833
+
834
+ /**
835
+ * Identity function that provides full type inference for subagent configurations.
836
+ * Verifies the workflow function's input parameters match the configured context,
837
+ * and properly types the lifecycle hooks with Task tool args and inferred result type.
838
+ *
839
+ * @example
840
+ * ```ts
841
+ * // With typed context — workflow must accept { prompt, context }
842
+ * const researcher = defineSubagent({
843
+ * name: "researcher",
844
+ * description: "Researches topics",
845
+ * workflow: researcherWorkflow, // (input: { prompt: string; context: { apiKey: string } }) => Promise<...>
846
+ * context: { apiKey: "..." },
847
+ * resultSchema: z.object({ findings: z.string() }),
848
+ * hooks: {
849
+ * onPostExecution: ({ result }) => {
850
+ * // result is typed as { findings: string }
851
+ * },
852
+ * },
853
+ * });
854
+ *
855
+ * // Without context — workflow only needs { prompt }
856
+ * const writer = defineSubagent({
857
+ * name: "writer",
858
+ * description: "Writes content",
859
+ * workflow: writerWorkflow, // (input: { prompt: string }) => Promise<...>
860
+ * resultSchema: z.object({ content: z.string() }),
861
+ * });
862
+ * ```
863
+ */
864
+ // With context — verifies workflow accepts { prompt, context: TContext }
865
+ export function defineSubagent<
866
+ TResult extends z.ZodType = z.ZodType,
867
+ TContext extends Record<string, unknown> = Record<string, unknown>,
868
+ >(
869
+ config: Omit<SubagentConfig<TResult>, "hooks" | "workflow" | "context"> & {
870
+ workflow:
871
+ | string
872
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
873
+ | ((input: { prompt: string; context: TContext }) => Promise<any>);
874
+ context: TContext;
875
+ hooks?: SubagentHooks<GenericTaskToolSchemaType, z.infer<TResult>>;
876
+ }
877
+ ): SubagentConfig<TResult>;
878
+ // Without context — verifies workflow accepts { prompt }
879
+ export function defineSubagent<TResult extends z.ZodType = z.ZodType>(
880
+ config: Omit<SubagentConfig<TResult>, "hooks" | "workflow"> & {
881
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
882
+ workflow: string | ((input: { prompt: string }) => Promise<any>);
883
+ hooks?: SubagentHooks<GenericTaskToolSchemaType, z.infer<TResult>>;
884
+ }
885
+ ): SubagentConfig<TResult>;
886
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
887
+ export function defineSubagent(config: any): SubagentConfig {
888
+ return config;
889
+ }
890
+
709
891
  /**
710
892
  * Utility to check if there were no tool calls besides a specific one
711
893
  */
package/src/lib/types.ts CHANGED
@@ -4,11 +4,11 @@ import type {
4
4
  InferToolResults,
5
5
  ParsedToolCallUnion,
6
6
  ToolCallResultUnion,
7
- ToolDefinition,
8
7
  ToolMap,
9
8
  } from "./tool-router";
10
9
 
11
10
  import type { MessageContent, StoredMessage } from "@langchain/core/messages";
11
+ import type { Workflow } from "@temporalio/workflow";
12
12
  import type { z } from "zod";
13
13
 
14
14
  /**
@@ -25,7 +25,7 @@ export type AgentStatus =
25
25
  * Base state that all agents must have
26
26
  */
27
27
  export interface BaseAgentState {
28
- tools: ToolDefinition[];
28
+ tools: SerializableToolDefinition[];
29
29
  status: AgentStatus;
30
30
  version: number;
31
31
  turns: number;
@@ -103,6 +103,19 @@ export interface ZeitlichAgentConfig<T extends ToolMap> {
103
103
  };
104
104
  }
105
105
 
106
+ /**
107
+ * A JSON-serializable tool definition for state storage.
108
+ * Uses a plain JSON Schema object instead of a live Zod instance,
109
+ * so it survives Temporal serialization without losing constraints (min, max, etc.).
110
+ */
111
+ export interface SerializableToolDefinition {
112
+ name: string;
113
+ description: string;
114
+ schema: Record<string, unknown>;
115
+ strict?: boolean;
116
+ max_uses?: number;
117
+ }
118
+
106
119
  /**
107
120
  * Configuration passed to runAgent activity
108
121
  */
@@ -142,12 +155,44 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
142
155
  name: string;
143
156
  /** Description shown to the parent agent explaining what this subagent does */
144
157
  description: string;
145
- /** Temporal workflow type name (used with executeChild) */
146
- workflowType: string;
158
+ /** Temporal workflow function or type name (used with executeChild) */
159
+ workflow: string | Workflow;
147
160
  /** Optional task queue - defaults to parent's queue if not specified */
148
161
  taskQueue?: string;
149
162
  /** Optional Zod schema to validate the child workflow's result. If omitted, result is passed through as-is. */
150
163
  resultSchema?: TResult;
164
+ /** Optional static context passed to the subagent on every invocation */
165
+ context?: Record<string, unknown>;
166
+ /** Per-subagent lifecycle hooks */
167
+ hooks?: SubagentHooks;
168
+ }
169
+
170
+ /**
171
+ * Per-subagent lifecycle hooks - defined on a SubagentConfig.
172
+ * Runs in addition to global hooks (global pre → subagent pre → execute → subagent post → global post).
173
+ */
174
+ export interface SubagentHooks<TArgs = unknown, TResult = unknown> {
175
+ /** Called before this subagent executes - can skip or modify args */
176
+ onPreExecution?: (ctx: {
177
+ args: TArgs;
178
+ threadId: string;
179
+ turn: number;
180
+ }) => PreToolUseHookResult | Promise<PreToolUseHookResult>;
181
+ /** Called after this subagent executes successfully */
182
+ onPostExecution?: (ctx: {
183
+ args: TArgs;
184
+ result: TResult;
185
+ threadId: string;
186
+ turn: number;
187
+ durationMs: number;
188
+ }) => void | Promise<void>;
189
+ /** Called when this subagent execution fails */
190
+ onExecutionFailure?: (ctx: {
191
+ args: TArgs;
192
+ error: Error;
193
+ threadId: string;
194
+ turn: number;
195
+ }) => PostToolUseFailureHookResult | Promise<PostToolUseFailureHookResult>;
151
196
  }
152
197
 
153
198
  /**
@@ -156,6 +201,8 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
156
201
  export interface SubagentInput {
157
202
  /** The prompt/task from the parent agent */
158
203
  prompt: string;
204
+ /** Optional context parameters passed from the parent agent */
205
+ context?: Record<string, unknown>;
159
206
  }
160
207
 
161
208
  // ============================================================================
@@ -328,6 +375,34 @@ export type SessionEndHook = (
328
375
  ctx: SessionEndHookContext
329
376
  ) => void | Promise<void>;
330
377
 
378
+ /**
379
+ * Per-tool lifecycle hooks - defined directly on a tool definition.
380
+ * Runs in addition to global hooks (global pre → tool pre → execute → tool post → global post).
381
+ */
382
+ export interface ToolHooks<TArgs = unknown, TResult = unknown> {
383
+ /** Called before this tool executes - can skip or modify args */
384
+ onPreToolUse?: (ctx: {
385
+ args: TArgs;
386
+ threadId: string;
387
+ turn: number;
388
+ }) => PreToolUseHookResult | Promise<PreToolUseHookResult>;
389
+ /** Called after this tool executes successfully */
390
+ onPostToolUse?: (ctx: {
391
+ args: TArgs;
392
+ result: TResult | null;
393
+ threadId: string;
394
+ turn: number;
395
+ durationMs: number;
396
+ }) => void | Promise<void>;
397
+ /** Called when this tool execution fails */
398
+ onPostToolUseFailure?: (ctx: {
399
+ args: TArgs;
400
+ error: Error;
401
+ threadId: string;
402
+ turn: number;
403
+ }) => PostToolUseFailureHookResult | Promise<PostToolUseFailureHookResult>;
404
+ }
405
+
331
406
  /**
332
407
  * Combined hooks interface for session lifecycle
333
408
  */
@@ -21,5 +21,5 @@ export const handleAskUserQuestionToolResult: ActivityToolHandler<
21
21
  }).toDict()
22
22
  );
23
23
 
24
- return { content: "Question submitted", result: { chatMessages: messages } };
24
+ return { toolResponse: "Question submitted", data: { chatMessages: messages } };
25
25
  };