@hazeljs/agent 0.7.9 → 0.8.1

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.
Files changed (47) hide show
  1. package/.eslintrc.js +4 -0
  2. package/IMPLEMENTATION_SUMMARY.md +36 -11
  3. package/PERSISTENCE.md +6 -0
  4. package/PRISMA_INTEGRATION.md +10 -5
  5. package/PRODUCTION_READINESS.md +37 -0
  6. package/QUICKSTART.md +12 -8
  7. package/README.md +72 -57
  8. package/STATE_VS_MEMORY.md +29 -12
  9. package/benchmarks/performance.benchmark.ts +1 -5
  10. package/coverage/clover.xml +616 -601
  11. package/coverage/lcov-report/index.html +33 -33
  12. package/coverage/lcov.info +957 -927
  13. package/dist/agent.module.js +2 -2
  14. package/dist/agent.module.js.map +1 -1
  15. package/dist/decorators/approval.decorator.d.ts +14 -0
  16. package/dist/decorators/approval.decorator.d.ts.map +1 -0
  17. package/dist/decorators/approval.decorator.js +20 -0
  18. package/dist/decorators/approval.decorator.js.map +1 -0
  19. package/dist/evaluation/agent-eval.d.ts +17 -0
  20. package/dist/evaluation/agent-eval.d.ts.map +1 -0
  21. package/dist/evaluation/agent-eval.js +18 -0
  22. package/dist/evaluation/agent-eval.js.map +1 -0
  23. package/dist/executor/agent.executor.d.ts +2 -0
  24. package/dist/executor/agent.executor.d.ts.map +1 -1
  25. package/dist/executor/agent.executor.js +4 -0
  26. package/dist/executor/agent.executor.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/runtime/agent.runtime.d.ts.map +1 -1
  32. package/dist/runtime/agent.runtime.js +1 -2
  33. package/dist/runtime/agent.runtime.js.map +1 -1
  34. package/dist/state/agent.state.d.ts.map +1 -1
  35. package/dist/state/agent.state.js +8 -7
  36. package/dist/state/agent.state.js.map +1 -1
  37. package/dist/state/database-state.manager.d.ts.map +1 -1
  38. package/dist/state/database-state.manager.js +8 -7
  39. package/dist/state/database-state.manager.js.map +1 -1
  40. package/dist/state/redis-state.manager.d.ts.map +1 -1
  41. package/dist/state/redis-state.manager.js +8 -7
  42. package/dist/state/redis-state.manager.js.map +1 -1
  43. package/dist/utils/circuit-breaker.d.ts +1 -1
  44. package/dist/utils/circuit-breaker.js +1 -1
  45. package/logs/combined.log +1 -1
  46. package/package.json +13 -7
  47. package/tsconfig.tsbuildinfo +1 -1
package/README.md CHANGED
@@ -15,6 +15,7 @@ Part of the HazelJS AI-Native Backend Framework. Stateful, tool-using, memory-en
15
15
  Built for **AI-native applications** - not just another agent framework. When you combine @hazeljs/agent with @hazeljs/core, @hazeljs/ai, and @hazeljs/rag, you get a complete stack for intelligent backends.
16
16
 
17
17
  **Perfect for:**
18
+
18
19
  - AI startups building production agent systems
19
20
  - Teams creating customer support or automation agents
20
21
  - Developers who want stateful, long-running workflows
@@ -67,10 +68,10 @@ export class SupportAgent {
67
68
 
68
69
  @Tool({
69
70
  description: 'Process a refund for an order',
70
- requiresApproval: true, // requires human approval before execution
71
+ requiresApproval: true, // requires human approval before execution
71
72
  parameters: [
72
73
  { name: 'orderId', type: 'string', required: true },
73
- { name: 'amount', type: 'number', required: true },
74
+ { name: 'amount', type: 'number', required: true },
74
75
  ],
75
76
  })
76
77
  async processRefund(input: { orderId: string; amount: number }) {
@@ -101,11 +102,11 @@ runtime.registerAgentInstance('support-agent', agent);
101
102
  ### 3. Execute
102
103
 
103
104
  ```typescript
104
- const result = await runtime.execute(
105
- 'support-agent',
106
- 'I need to check my order #12345',
107
- { sessionId: 'user-session-123', userId: 'user-456', enableMemory: true },
108
- );
105
+ const result = await runtime.execute('support-agent', 'I need to check my order #12345', {
106
+ sessionId: 'user-session-123',
107
+ userId: 'user-456',
108
+ enableMemory: true,
109
+ });
109
110
 
110
111
  console.log(result.response);
111
112
  console.log(`Completed in ${result.steps.length} steps`);
@@ -170,7 +171,10 @@ export class OrchestratorAgent {
170
171
 
171
172
  @Agent({ name: 'ResearchAgent', systemPrompt: 'You are an expert researcher.' })
172
173
  export class ResearchAgent {
173
- @Tool({ description: 'Search the web', parameters: [{ name: 'query', type: 'string', required: true }] })
174
+ @Tool({
175
+ description: 'Search the web',
176
+ parameters: [{ name: 'query', type: 'string', required: true }],
177
+ })
174
178
  async searchWeb(input: { query: string }) {
175
179
  return `Research findings for: ${input.query}`;
176
180
  }
@@ -178,7 +182,10 @@ export class ResearchAgent {
178
182
 
179
183
  @Agent({ name: 'WriterAgent', systemPrompt: 'You are a professional technical writer.' })
180
184
  export class WriterAgent {
181
- @Tool({ description: 'Format content as Markdown', parameters: [{ name: 'raw', type: 'string', required: true }] })
185
+ @Tool({
186
+ description: 'Format content as Markdown',
187
+ parameters: [{ name: 'raw', type: 'string', required: true }],
188
+ })
182
189
  async formatMarkdown(input: { raw: string }) {
183
190
  return `## Article\n\n${input.raw}`;
184
191
  }
@@ -189,12 +196,15 @@ export class WriterAgent {
189
196
 
190
197
  ```typescript
191
198
  const orchestrator = new OrchestratorAgent();
192
- const researcher = new ResearchAgent();
193
- const writer = new WriterAgent();
199
+ const researcher = new ResearchAgent();
200
+ const writer = new WriterAgent();
194
201
 
195
- [ResearchAgent, WriterAgent, OrchestratorAgent].forEach(A => runtime.registerAgent(A));
196
- [['OrchestratorAgent', orchestrator], ['ResearchAgent', researcher], ['WriterAgent', writer]]
197
- .forEach(([name, inst]) => runtime.registerAgentInstance(name as string, inst));
202
+ [ResearchAgent, WriterAgent, OrchestratorAgent].forEach((A) => runtime.registerAgent(A));
203
+ [
204
+ ['OrchestratorAgent', orchestrator],
205
+ ['ResearchAgent', researcher],
206
+ ['WriterAgent', writer],
207
+ ].forEach(([name, inst]) => runtime.registerAgentInstance(name as string, inst));
198
208
 
199
209
  const result = await runtime.execute('OrchestratorAgent', 'Write a blog post about LLMs');
200
210
  console.log(result.response);
@@ -227,7 +237,7 @@ const graph = runtime.createGraph('research-pipeline');
227
237
  const pipeline = runtime
228
238
  .createGraph('blog-pipeline')
229
239
  .addNode('researcher', { type: 'agent', agentName: 'ResearchAgent' })
230
- .addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
240
+ .addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
231
241
  .addEdge('researcher', 'writer')
232
242
  .addEdge('writer', END)
233
243
  .setEntryPoint('researcher')
@@ -243,13 +253,11 @@ console.log(result.output);
243
253
  const router = runtime
244
254
  .createGraph('router')
245
255
  .addNode('classifier', { type: 'agent', agentName: 'ClassifierAgent' })
246
- .addNode('coder', { type: 'agent', agentName: 'CoderAgent' })
247
- .addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
256
+ .addNode('coder', { type: 'agent', agentName: 'CoderAgent' })
257
+ .addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
248
258
  .setEntryPoint('classifier')
249
- .addConditionalEdge('classifier', (state) =>
250
- state.data?.type === 'code' ? 'coder' : 'writer',
251
- )
252
- .addEdge('coder', END)
259
+ .addConditionalEdge('classifier', (state) => (state.data?.type === 'code' ? 'coder' : 'writer'))
260
+ .addEdge('coder', END)
253
261
  .addEdge('writer', END)
254
262
  .compile();
255
263
 
@@ -265,19 +273,19 @@ async function splitTask(state: GraphState) {
265
273
 
266
274
  async function mergeResults(state: GraphState) {
267
275
  const results = state.data?.branchResults as ParallelBranchResult[];
268
- return { ...state, output: results.map(r => r.output).join('\n---\n') };
276
+ return { ...state, output: results.map((r) => r.output).join('\n---\n') };
269
277
  }
270
278
 
271
279
  const parallel = runtime
272
280
  .createGraph('parallel-research')
273
- .addNode('splitter', { type: 'function', fn: splitTask })
281
+ .addNode('splitter', { type: 'function', fn: splitTask })
274
282
  .addNode('parallel-1', { type: 'parallel', branches: ['tech-researcher', 'market-researcher'] })
275
- .addNode('tech-researcher', { type: 'agent', agentName: 'TechResearchAgent' })
283
+ .addNode('tech-researcher', { type: 'agent', agentName: 'TechResearchAgent' })
276
284
  .addNode('market-researcher', { type: 'agent', agentName: 'MarketResearchAgent' })
277
- .addNode('combiner', { type: 'function', fn: mergeResults })
278
- .addEdge('splitter', 'parallel-1')
285
+ .addNode('combiner', { type: 'function', fn: mergeResults })
286
+ .addEdge('splitter', 'parallel-1')
279
287
  .addEdge('parallel-1', 'combiner')
280
- .addEdge('combiner', END)
288
+ .addEdge('combiner', END)
281
289
  .setEntryPoint('splitter')
282
290
  .compile();
283
291
 
@@ -308,7 +316,7 @@ interface AgentGraph {
308
316
  interface CompiledGraph {
309
317
  execute(input: string, options?: GraphExecutionOptions): Promise<GraphExecutionResult>;
310
318
  stream(input: string, options?: GraphExecutionOptions): AsyncIterable<GraphStreamChunk>;
311
- visualize(): string; // returns a Mermaid diagram string
319
+ visualize(): string; // returns a Mermaid diagram string
312
320
  }
313
321
  ```
314
322
 
@@ -346,26 +354,27 @@ const supervisor = runtime.createSupervisor({
346
354
  },
347
355
  });
348
356
 
349
- const result = await supervisor.run(
350
- 'Build and document a REST API for a todo app',
351
- { sessionId: 'proj-001' },
352
- );
357
+ const result = await supervisor.run('Build and document a REST API for a todo app', {
358
+ sessionId: 'proj-001',
359
+ });
353
360
 
354
361
  console.log(result.response);
355
362
  result.rounds.forEach((round, i) => {
356
- console.log(`Round ${i + 1}: routed to ${round.worker} — ${round.workerResult.response.slice(0, 80)}`);
363
+ console.log(
364
+ `Round ${i + 1}: routed to ${round.worker} — ${round.workerResult.response.slice(0, 80)}`
365
+ );
357
366
  });
358
367
  ```
359
368
 
360
369
  **`SupervisorConfig`:**
361
370
 
362
- | Field | Type | Default | Description |
363
- |-------|------|---------|-------------|
364
- | `name` | string | — | Supervisor instance name |
365
- | `workers` | string[] | — | Registered agent names available to the supervisor |
366
- | `maxRounds` | number | 5 | Maximum routing iterations |
367
- | `llm` | `(prompt: string) => Promise<string>` | — | LLM function for routing decisions |
368
- | `sessionId` | string | auto | Session for memory continuity across rounds |
371
+ | Field | Type | Default | Description |
372
+ | ----------- | ------------------------------------- | ------- | -------------------------------------------------- |
373
+ | `name` | string | — | Supervisor instance name |
374
+ | `workers` | string[] | — | Registered agent names available to the supervisor |
375
+ | `maxRounds` | number | 5 | Maximum routing iterations |
376
+ | `llm` | `(prompt: string) => Promise<string>` | — | LLM function for routing decisions |
377
+ | `sessionId` | string | auto | Session for memory continuity across rounds |
369
378
 
370
379
  ---
371
380
 
@@ -412,17 +421,17 @@ idle → thinking → using_tool → thinking → ... → completed
412
421
  ```typescript
413
422
  import { AgentEventType } from '@hazeljs/agent';
414
423
 
415
- runtime.on(AgentEventType.EXECUTION_STARTED, e => console.log('started:', e.data));
416
- runtime.on(AgentEventType.EXECUTION_COMPLETED, e => console.log('completed:', e.data));
417
- runtime.on(AgentEventType.STEP_STARTED, e => console.log('step:', e.data));
418
- runtime.on(AgentEventType.TOOL_EXECUTION_STARTED, e => console.log('tool:', e.data));
419
- runtime.on(AgentEventType.TOOL_APPROVAL_REQUESTED, e => {
424
+ runtime.on(AgentEventType.EXECUTION_STARTED, (e) => console.log('started:', e.data));
425
+ runtime.on(AgentEventType.EXECUTION_COMPLETED, (e) => console.log('completed:', e.data));
426
+ runtime.on(AgentEventType.STEP_STARTED, (e) => console.log('step:', e.data));
427
+ runtime.on(AgentEventType.TOOL_EXECUTION_STARTED, (e) => console.log('tool:', e.data));
428
+ runtime.on(AgentEventType.TOOL_APPROVAL_REQUESTED, (e) => {
420
429
  console.log('approval needed:', e.data);
421
430
  runtime.approveToolExecution(e.data.requestId, 'admin');
422
431
  });
423
432
 
424
433
  // Catch-all
425
- runtime.onAny(e => console.log(e.type, e.data));
434
+ runtime.onAny((e) => console.log(e.type, e.data));
426
435
  ```
427
436
 
428
437
  ---
@@ -436,7 +445,9 @@ import { RagModule } from '@hazeljs/rag';
436
445
 
437
446
  @HazelModule({
438
447
  imports: [
439
- RagModule.forRoot({ /* ... */ }),
448
+ RagModule.forRoot({
449
+ /* ... */
450
+ }),
440
451
  AgentModule.forRoot({
441
452
  runtime: { defaultMaxSteps: 10, enableObservability: true },
442
453
  agents: [SupportAgent, ResearchAgent, WriterAgent, OrchestratorAgent],
@@ -467,11 +478,11 @@ Keep each agent focused on one domain. `@Delegate` lets the orchestrator combine
467
478
 
468
479
  ### Choose the right multi-agent pattern
469
480
 
470
- | Pattern | Use when |
471
- |---------|----------|
472
- | `@Delegate` | Two or three agents with a clear orchestrator / worker split |
473
- | `AgentGraph` | Workflow is known at design time; conditional routing matters |
474
- | `SupervisorAgent` | Task decomposition is dynamic; you want LLM-driven routing |
481
+ | Pattern | Use when |
482
+ | ----------------- | ------------------------------------------------------------- |
483
+ | `@Delegate` | Two or three agents with a clear orchestrator / worker split |
484
+ | `AgentGraph` | Workflow is known at design time; conditional routing matters |
485
+ | `SupervisorAgent` | Task decomposition is dynamic; you want LLM-driven routing |
475
486
 
476
487
  ### Require approval for destructive actions
477
488
 
@@ -501,7 +512,11 @@ async callExternalAPI(input: { endpoint: string }) {
501
512
 
502
513
  ```typescript
503
514
  class AgentRuntime {
504
- execute(agentName: string, input: string, options?: ExecuteOptions): Promise<AgentExecutionResult>;
515
+ execute(
516
+ agentName: string,
517
+ input: string,
518
+ options?: ExecuteOptions
519
+ ): Promise<AgentExecutionResult>;
505
520
  resume(executionId: string, input?: string): Promise<AgentExecutionResult>;
506
521
  registerAgent(agentClass: new (...args: unknown[]) => unknown): void;
507
522
  registerAgentInstance(name: string, instance: unknown): void;
@@ -516,10 +531,10 @@ class AgentRuntime {
516
531
 
517
532
  ### Decorators
518
533
 
519
- | Decorator | Target | Description |
520
- |-----------|--------|-------------|
521
- | `@Agent(config)` | Class | Declares a class as an agent |
522
- | `@Tool(config)` | Method | Exposes a method as an LLM-callable tool |
534
+ | Decorator | Target | Description |
535
+ | ------------------- | ------ | ------------------------------------------------------------------------ |
536
+ | `@Agent(config)` | Class | Declares a class as an agent |
537
+ | `@Tool(config)` | Method | Exposes a method as an LLM-callable tool |
523
538
  | `@Delegate(config)` | Method | Delegates a method to another agent (registers as `@Tool` automatically) |
524
539
 
525
540
  ### `GraphNodeConfig` types
@@ -27,6 +27,7 @@ There are **two separate persistence layers** in the agent system that serve dif
27
27
  **Purpose**: Track the **execution flow** and **state machine** of agent runs
28
28
 
29
29
  **What it stores**:
30
+
30
31
  - ✅ Execution ID and context
31
32
  - ✅ Current state (IDLE, THINKING, WAITING_FOR_APPROVAL, etc.)
32
33
  - ✅ Execution steps (what the agent did)
@@ -34,11 +35,13 @@ There are **two separate persistence layers** in the agent system that serve dif
34
35
  - ✅ Execution metadata
35
36
  - ✅ **Temporary** conversation history (during execution)
36
37
 
37
- **Lifetime**:
38
+ **Lifetime**:
39
+
38
40
  - **Short-lived** - typically minutes to hours
39
41
  - Ephemeral - deleted after execution completes (or TTL expires)
40
42
 
41
43
  **Use cases**:
44
+
42
45
  - Resume paused executions
43
46
  - Track execution progress
44
47
  - Debug failed executions
@@ -46,11 +49,13 @@ There are **two separate persistence layers** in the agent system that serve dif
46
49
  - State machine transitions
47
50
 
48
51
  **Backends**:
52
+
49
53
  - In-Memory (default)
50
54
  - Redis (production)
51
55
  - Database/Prisma (audit)
52
56
 
53
57
  **Example**:
58
+
54
59
  ```typescript
55
60
  // Agent State tracks: "Agent executed step 1, 2, 3, now waiting for approval"
56
61
  {
@@ -69,6 +74,7 @@ There are **two separate persistence layers** in the agent system that serve dif
69
74
  **Purpose**: Store **long-term knowledge** and **context** across sessions
70
75
 
71
76
  **What it stores**:
77
+
72
78
  - ✅ Conversation history (across all sessions)
73
79
  - ✅ Entities (people, places, things mentioned)
74
80
  - ✅ Facts (learned information)
@@ -76,11 +82,13 @@ There are **two separate persistence layers** in the agent system that serve dif
76
82
  - ✅ Events (important occurrences)
77
83
 
78
84
  **Lifetime**:
85
+
79
86
  - **Long-lived** - days, weeks, months
80
87
  - Persistent - survives agent restarts
81
88
  - Cross-session - shared across multiple agent runs
82
89
 
83
90
  **Use cases**:
91
+
84
92
  - Build context for new conversations
85
93
  - Remember entities across sessions
86
94
  - Store learned facts
@@ -88,11 +96,13 @@ There are **two separate persistence layers** in the agent system that serve dif
88
96
  - Semantic search of past conversations
89
97
 
90
98
  **Backends**:
99
+
91
100
  - BufferMemory (in-memory)
92
101
  - VectorMemory (Pinecone, Weaviate, Qdrant, ChromaDB)
93
102
  - HybridMemory (combination)
94
103
 
95
104
  **Example**:
105
+
96
106
  ```typescript
97
107
  // Memory stores: "User's name is John, likes coffee, mentioned Paris last week"
98
108
  {
@@ -107,15 +117,15 @@ There are **two separate persistence layers** in the agent system that serve dif
107
117
 
108
118
  ## Key Differences
109
119
 
110
- | Aspect | Agent State | Memory (RAG) |
111
- |--------|-------------|--------------|
112
- | **Purpose** | Execution flow tracking | Long-term knowledge |
113
- | **Lifetime** | Minutes to hours | Days to months |
114
- | **Scope** | Single execution | Cross-session |
115
- | **Data** | Steps, state, metadata | Conversations, entities, facts |
116
- | **Query** | By executionId | Semantic search, by sessionId |
117
- | **Backend** | Redis/DB | Vector stores (Pinecone, etc.) |
118
- | **When used** | During execution | Before/after execution |
120
+ | Aspect | Agent State | Memory (RAG) |
121
+ | ------------- | ----------------------- | ------------------------------ |
122
+ | **Purpose** | Execution flow tracking | Long-term knowledge |
123
+ | **Lifetime** | Minutes to hours | Days to months |
124
+ | **Scope** | Single execution | Cross-session |
125
+ | **Data** | Steps, state, metadata | Conversations, entities, facts |
126
+ | **Query** | By executionId | Semantic search, by sessionId |
127
+ | **Backend** | Redis/DB | Vector stores (Pinecone, etc.) |
128
+ | **When used** | During execution | Before/after execution |
119
129
 
120
130
  ## How They Work Together
121
131
 
@@ -179,12 +189,14 @@ await contextBuilder.persistToMemory(context);
179
189
  **Both** store conversation history, but for different reasons:
180
190
 
181
191
  ### Agent State Conversation History
192
+
182
193
  - **Purpose**: Track messages **during** execution
183
194
  - **Scope**: Single execution only
184
195
  - **Lifetime**: Until execution completes
185
196
  - **Use**: Resume paused executions, debug current run
186
197
 
187
198
  ### Memory Conversation History
199
+
188
200
  - **Purpose**: Build context for **future** conversations
189
201
  - **Scope**: All sessions for a user/session
190
202
  - **Lifetime**: Long-term (weeks/months)
@@ -193,6 +205,7 @@ await contextBuilder.persistToMemory(context);
193
205
  ## When to Use Which
194
206
 
195
207
  ### Use Agent State Persistence when:
208
+
196
209
  - ✅ You need to resume paused executions
197
210
  - ✅ You want to track execution progress
198
211
  - ✅ You need to debug failed runs
@@ -200,6 +213,7 @@ await contextBuilder.persistToMemory(context);
200
213
  - ✅ You need execution audit trails
201
214
 
202
215
  ### Use Memory Persistence when:
216
+
203
217
  - ✅ You want agents to remember past conversations
204
218
  - ✅ You need entity tracking across sessions
205
219
  - ✅ You want to store learned facts
@@ -209,6 +223,7 @@ await contextBuilder.persistToMemory(context);
209
223
  ## Recommended Setup
210
224
 
211
225
  ### Development
226
+
212
227
  ```typescript
213
228
  // In-memory for both (default)
214
229
  const runtime = new AgentRuntime({
@@ -218,6 +233,7 @@ const runtime = new AgentRuntime({
218
233
  ```
219
234
 
220
235
  ### Production
236
+
221
237
  ```typescript
222
238
  // Redis for agent state (fast, distributed)
223
239
  // Vector store (Pinecone) for memory (semantic search)
@@ -226,8 +242,8 @@ const memoryStore = new VectorMemory(pineconeStore, embeddings);
226
242
  const memoryManager = new MemoryManager(memoryStore);
227
243
 
228
244
  const runtime = new AgentRuntime({
229
- stateManager, // ← Agent execution state
230
- memoryManager, // ← Long-term memory
245
+ stateManager, // ← Agent execution state
246
+ memoryManager, // ← Long-term memory
231
247
  });
232
248
  ```
233
249
 
@@ -237,6 +253,7 @@ const runtime = new AgentRuntime({
237
253
  - **Memory** = "What does the agent know from past conversations?"
238
254
 
239
255
  They complement each other:
256
+
240
257
  - **State** enables resumable, trackable executions
241
258
  - **Memory** enables context-aware, continuous conversations
242
259
 
@@ -57,11 +57,7 @@ class PerformanceBenchmark {
57
57
  * Format benchmark results
58
58
  */
59
59
  formatResults(results: BenchmarkResult[]): string {
60
- const lines: string[] = [
61
- 'Performance Benchmark Results',
62
- '============================',
63
- '',
64
- ];
60
+ const lines: string[] = ['Performance Benchmark Results', '============================', ''];
65
61
 
66
62
  for (const result of results) {
67
63
  lines.push(`${result.name}:`);