@dbx-tools/appkit-mastra 0.1.18 → 0.1.19

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/README.md CHANGED
@@ -190,65 +190,72 @@ that implements the standard `getAgentTools()` + `executeAgentTool()` +
190
190
  `executeAgentTool`, so OBO auth (`asUser`) and telemetry spans stay
191
191
  intact.
192
192
 
193
- `plugins.genie` is special-cased: it swaps the generic AppKit toolkit
194
- (which only emits a single final result chunk per call) for a
195
- streaming-aware tools record built on top of the plugin's
196
- `exports().sendMessage` AsyncGenerator. Each Genie wire event
197
- (`FETCHING_METADATA`, `ASKING_AI`, `EXECUTING_QUERY`, attached SQL,
198
- errors) is normalised into a `GenieProgress` payload and pushed
199
- mid-flight through Mastra's `ctx.writer`, surfacing as
200
- `tool-output` chunks the React client can render as inline status
201
- pills and SQL blocks while the LLM is still waiting on the final
202
- `tool-result`. Tool ids are stable: `genie` for the default alias,
203
- `genie-<alias>` for additional aliases, and one shared
204
- `genie_get_conversation`.
205
-
206
- Genie tool-result shape (LLM-bound):
193
+ `plugins.genie` is special-cased: it talks to Genie directly via
194
+ `@dbx-tools/genie` (`genieEventChat`) rather than calling AppKit's
195
+ stock `genie` toolkit. Each invocation spins up a per-call inner
196
+ Mastra `Agent` with three tools (`ask_genie`,
197
+ `get_space_description`, `get_space_serialized`), runs it with
198
+ `structuredOutput`, and produces a hydrated
199
+ {@link GenieAgentResult}. AppKit's `genie()` plugin is honored
200
+ only for its `spaces` config (and the matching
201
+ `app.yaml`-declared resources). Tool ids are stable: `genie` for
202
+ the default alias, `genie_<alias>` for additional aliases.
203
+
204
+ Genie tool-result shape (LLM-bound) is the
205
+ [`GenieAgentResult`](../appkit-mastra-shared/src/genie.ts) type:
207
206
 
208
- ```
209
- {
210
- conversationId?: string, // pass back to continue the same Genie thread
211
- genieAnswer?: string, // Genie's prose answer; pass through verbatim
212
- datasets?: [{ // metadata only, one per executed SQL statement
213
- chartId: string, // embed [[chart:<chartId>]] to render inline
214
- title?: string,
215
- description?: string,
216
- columns: string[],
217
- rowCount: number,
218
- sql?: string,
219
- }],
220
- suggestedFollowUps?: string[], // UI shows as buttons; don't list in reply
221
- error?: string,
222
- }
207
+ ```ts
208
+ type GenieAgentResult = {
209
+ spaceId: string;
210
+ conversationId?: string; // thread back to continue the same Genie thread
211
+ summary: Array< // ordered prose + visualize slots
212
+ | { type: "string"; text: string }
213
+ | {
214
+ type: "visualize";
215
+ statementId: string;
216
+ title?: string;
217
+ description?: string;
218
+ dataset: {
219
+ data: { columns: string[]; rows: Row[]; rowCount: number };
220
+ chart?: { chartId: string; chartType: "bar" | "line" | "area" | "scatter" | "pie" };
221
+ };
222
+ }
223
+ >;
224
+ error?: string;
225
+ };
223
226
  ```
224
227
 
225
- `datasets[]` is metadata only - column names, row count, the SQL
226
- Genie ran. The actual rows ride a separate `type: "chart"` writer
227
- event so the LLM never has them in context (token cost stays flat
228
- regardless of dataset size). The model references each dataset by
229
- its `chartId` via the `[[chart:<chartId>]]` marker to display the
230
- chart inline; see the `render_data` section below for how the
231
- client resolves those markers.
232
-
233
- Genie data flow:
234
-
235
- - The writer emits `type: "started" | "status" | "sql" |
236
- "suggested" | "error"` events for the live loading pill. SQL
237
- text is shown via a small Shiki-highlighted block.
238
- - The writer **also** emits two `type: "chart"` events per
239
- executed SQL statement, sharing the same `chartId`: the first
240
- carries `{chartId, title, description?, data}` (rows converted
241
- to objects keyed by column name with best-effort numeric
242
- coercion); the second, when the chart-planner agent finishes,
243
- carries `{chartId, option}` (the resolved Echarts spec). The
244
- chat client's `<ChartSlot>` merges them by `chartId` and
245
- renders inline at the matching `[[chart:<chartId>]]` marker.
246
- This is the exact same wire format as the `render_data` tool,
247
- so Genie and hand-built charts are indistinguishable on the
248
- client.
249
- - After a hard reload, `synthesizeToolEventsFromHistory` rebuilds
250
- `suggested` events from the persisted tool-result; the SQL pill
251
- and chart events are live-only and don't replay.
228
+ The `summary` is the user-facing renderable: a mixed sequence of
229
+ prose paragraphs (`type: "string"`) and `visualize` slots that
230
+ mark where a chart should appear in the model's final answer.
231
+ Visualize slots embed the dataset (rows + columns) plus a slim
232
+ `chart` reference (just `chartId` + `chartType`); the resolved
233
+ Echarts spec rides a separate `type: "chart"` writer event so the
234
+ LLM never holds it in context. The host UI joins the dataset and
235
+ the writer event by `chartId` and renders inline at the matching
236
+ `[[chart:<chartId>]]` marker.
237
+
238
+ Genie writer event flow:
239
+
240
+ - The writer emits the flat
241
+ [`GenieWriterEvent`](../appkit-mastra-shared/src/genie.ts)
242
+ union for the live loading pill - the wire-derived
243
+ `GenieChatEvent` (status / thinking / sql / rows / suggested)
244
+ plus the Mastra-only lifecycle events (`started`,
245
+ `ask_genie_done`, `summary`, `chart`, `error`).
246
+ - The writer emits one `type: "chart"` event per executed SQL
247
+ statement carrying `{chartId, title, description?, data,
248
+ option, statementId, messageId}`. The chat client's
249
+ `<ChartSlot>` renders inline at the matching
250
+ `[[chart:<chartId>]]` marker. This is the exact same wire
251
+ format as the `render_data` tool, so Genie and hand-built
252
+ charts are indistinguishable on the client.
253
+ - After a hard reload, `synthesizeToolEventsFromHistory`
254
+ reconstructs lifecycle events from the persisted summary
255
+ (`error` events via `genieResultToWriterEvents`); live-only
256
+ events (rows, SQL pill, chart specs) don't replay because the
257
+ resolved Echarts spec is held off-band on the per-request
258
+ `RequestContext`.
252
259
 
253
260
  ### `render_data` (system-default ambient tool)
254
261
 
@@ -710,20 +717,25 @@ function Chat() {
710
717
  mount, so a custom `mastra({ name: "myMastra" })` rewrites every path):
711
718
 
712
719
 
713
- | Field | Example | Description |
714
- | ------------------ | ----------------------------------- | -------------------------------------------------------- |
715
- | `basePath` | `"/api/mastra"` | Plugin mount path. |
716
- | `chatPath` | `"/api/mastra/route/chat"` | Default-agent chat URL. Use `chatUrl(config)` to get it. |
717
- | `chatPathTemplate` | `"/api/mastra/route/chat/:agentId"` | OpenAPI-style template for tools / docs. |
718
- | `modelsPath` | `"/api/mastra/models"` | `GET` cached endpoint catalogue. |
719
- | `defaultAgent` | `"analyst"` | Agent id `chatRoute` binds to when none is supplied. |
720
- | `agents` | `["analyst", "helper"]` | Every registered agent id in order. |
720
+ | Field | Example | Description |
721
+ | --------------------- | -------------------------------------- | -------------------------------------------------------------------- |
722
+ | `basePath` | `"/api/mastra"` | Plugin mount path. |
723
+ | `chatPath` | `"/api/mastra/route/chat"` | Default-agent chat URL. Use `chatUrl(config)` to get it. |
724
+ | `chatPathTemplate` | `"/api/mastra/route/chat/:agentId"` | OpenAPI-style template for tools / docs. |
725
+ | `modelsPath` | `"/api/mastra/models"` | `GET` cached endpoint catalogue. |
726
+ | `historyPath` | `"/api/mastra/route/history"` | Default-agent thread history. Use `historyUrl(config, opts)`. |
727
+ | `historyPathTemplate` | `"/api/mastra/route/history/:agentId"` | OpenAPI-style template for tools / docs. |
728
+ | `defaultAgent` | `"analyst"` | Agent id `chatRoute` binds to when none is supplied. |
729
+ | `agents` | `["analyst", "helper"]` | Every registered agent id in order. |
721
730
 
722
731
 
723
732
  `chatUrl(config, agentId?)` returns `config.chatPath` for the default
724
733
  agent (the registered `chatRoute` mount that omits `:agentId`), and
725
- `${config.chatPath}/${encodeURIComponent(agentId)}` otherwise. Pure
726
- function: no React, no hooks, safe in service workers and SSR.
734
+ `${config.chatPath}/${encodeURIComponent(agentId)}` otherwise. The
735
+ sibling `historyUrl(config, { agentId?, page?, perPage? })` mirrors
736
+ that pattern for the `/history` endpoint and appends pagination
737
+ query parameters when provided. Both are pure functions: no React,
738
+ no hooks, safe in service workers and SSR.
727
739
 
728
740
  ## License
729
741
 
@@ -389,13 +389,12 @@ function buildPluginsMap(config, context) {
389
389
  * Genie agent inherits the same model resolver / fallback
390
390
  * ladder the calling agents use.
391
391
  *
392
- * The legacy AppKit `genie` plugin's tools are no longer consulted
393
- * at runtime - the Genie agent talks to Genie directly via
394
- * `@dbx-tools/genie` (`genieEventChat`) and the workspace
395
- * `statementExecution.getStatement` API. But the plugin's
396
- * resource manifest and `spaces` config are still honored so
397
- * existing `app.yaml` configs and `genie({ spaces })` wiring
398
- * keep working without change.
392
+ * The Genie agent talks to Genie directly via `@dbx-tools/genie`
393
+ * (`genieEventChat`) and the workspace
394
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
395
+ * plugin is honored only for its resource manifest and `spaces`
396
+ * config so existing `app.yaml` configs and `genie({ spaces })`
397
+ * wiring keep working without change.
399
398
  */
400
399
  function resolveProvider(config, context, propName) {
401
400
  if (propName === "genie") {
package/dist/src/chart.js CHANGED
@@ -31,6 +31,7 @@ import { Agent } from "@mastra/core/agent";
31
31
  import { createTool } from "@mastra/core/tools";
32
32
  import { z } from "zod";
33
33
  import { ModelTier, modelForTier, buildModel } from "./model.js";
34
+ import { safeWrite } from "./writer.js";
34
35
  /**
35
36
  * Module-level logger tagged `[mastra/chart]`. Uses the shared
36
37
  * {@link logUtils.logger} so calls below `LOG_LEVEL` are
@@ -373,56 +374,33 @@ export function buildRenderDataTool(config) {
373
374
  // for the table-like fallback / hover, option for the
374
375
  // actual render. Best-effort write so a closed
375
376
  // downstream stream can't take the tool down.
376
- await safeWrite(writer, chartId, {
377
+ await safeWrite(log, writer, {
377
378
  type: "chart",
378
379
  chartId,
379
380
  title,
380
381
  ...(description ? { description } : {}),
381
382
  data,
382
383
  option,
383
- });
384
+ }, { chartId });
384
385
  }
385
386
  catch (err) {
386
387
  log.warn("render:error", {
387
388
  chartId,
388
389
  elapsedMs: Date.now() - startedAt,
389
- error: err instanceof Error ? err.message : String(err),
390
+ error: commonUtils.errorMessage(err),
390
391
  });
391
392
  // Surface as a writer-level error so the slot can
392
393
  // transition to "couldn't render chart" without the
393
394
  // parent agent surfacing a stack trace.
394
- await safeWrite(writer, chartId, {
395
+ await safeWrite(log, writer, {
395
396
  type: "error",
396
- error: err instanceof Error ? err.message : String(err),
397
- });
397
+ error: commonUtils.errorMessage(err),
398
+ }, { chartId });
398
399
  }
399
400
  return { chartId };
400
401
  },
401
402
  });
402
403
  }
403
- /**
404
- * Best-effort writer.write. Failures are logged at `warn` (a
405
- * persistently-closed writer is the most likely culprit when
406
- * chart events go missing client-side) but swallowed so a closed
407
- * downstream stream (cancelled request, client navigated away)
408
- * can't take a tool down.
409
- */
410
- async function safeWrite(writer, chartId, chunk) {
411
- if (!writer) {
412
- log.debug("write:no-writer", { chartId });
413
- return;
414
- }
415
- try {
416
- await writer.write(chunk);
417
- log.debug("write:ok", { chartId });
418
- }
419
- catch (err) {
420
- log.warn("write:error", {
421
- chartId,
422
- error: err instanceof Error ? err.message : String(err),
423
- });
424
- }
425
- }
426
404
  /**
427
405
  * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
428
406
  * JSON. Centralized here so the planner agent only fills in the
@@ -32,12 +32,11 @@
32
32
  * {@link GenieSummaryItem} `string` variant, and returns the
33
33
  * hydrated {@link GenieAgentResult}.
34
34
  *
35
- * The legacy AppKit `genie` plugin (`@databricks/appkit`'s `genie`)
36
- * is no longer used at runtime. The inner agent talks to Genie
37
- * directly via `@dbx-tools/genie` (`genieEventChat`) and the
38
- * workspace `statementExecution.getStatement` API. The plugin's
39
- * `spaces` config is still honored so existing AppKit-style wiring
40
- * keeps working without change.
35
+ * The inner agent talks to Genie directly via
36
+ * `@dbx-tools/genie` (`genieEventChat`) and the workspace
37
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
38
+ * plugin is honored only for its `spaces` config so existing
39
+ * AppKit-style wiring keeps working without change.
41
40
  */
42
41
  import { type ChartEvent } from "@dbx-tools/appkit-mastra-shared";
43
42
  import { appkitUtils } from "@dbx-tools/shared";
package/dist/src/genie.js CHANGED
@@ -32,12 +32,11 @@
32
32
  * {@link GenieSummaryItem} `string` variant, and returns the
33
33
  * hydrated {@link GenieAgentResult}.
34
34
  *
35
- * The legacy AppKit `genie` plugin (`@databricks/appkit`'s `genie`)
36
- * is no longer used at runtime. The inner agent talks to Genie
37
- * directly via `@dbx-tools/genie` (`genieEventChat`) and the
38
- * workspace `statementExecution.getStatement` API. The plugin's
39
- * `spaces` config is still honored so existing AppKit-style wiring
40
- * keeps working without change.
35
+ * The inner agent talks to Genie directly via
36
+ * `@dbx-tools/genie` (`genieEventChat`) and the workspace
37
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
38
+ * plugin is honored only for its `spaces` config so existing
39
+ * AppKit-style wiring keeps working without change.
41
40
  */
42
41
  import { CacheManager, genie } from "@databricks/appkit";
43
42
  import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
@@ -52,6 +51,7 @@ import { z } from "zod";
52
51
  import { runChartPlanner } from "./chart.js";
53
52
  import { MASTRA_USER_KEY } from "./config.js";
54
53
  import { buildModel } from "./model.js";
54
+ import { safeWrite } from "./writer.js";
55
55
  const log = logUtils.logger("mastra/genie");
56
56
  /** Default alias used when a single unnamed Genie space is wired up. */
57
57
  export const DEFAULT_GENIE_ALIAS = "default";
@@ -118,26 +118,6 @@ function extractStatementId(message) {
118
118
  }
119
119
  return undefined;
120
120
  }
121
- /**
122
- * Best-effort `writer.write`. The writer carries the unified flat
123
- * event vocabulary directly - no translation layer - so
124
- * subscribers narrow on `event.type` and read fields inline.
125
- * Failures (downstream stream closed, cancelled request) are
126
- * swallowed with a `warn` log so an in-flight Genie turn isn't
127
- * taken down by a navigated-away client.
128
- */
129
- async function safeWrite(writer, chunk) {
130
- if (!writer)
131
- return;
132
- try {
133
- await writer.write(chunk);
134
- }
135
- catch (err) {
136
- log.warn("writer:error", {
137
- error: err instanceof Error ? err.message : String(err),
138
- });
139
- }
140
- }
141
121
  /**
142
122
  * Lowercased placeholder strings we reject at the `ask_genie`
143
123
  * boundary so the LLM doesn't spend a Genie round-trip on a
@@ -279,7 +259,7 @@ async function readCachedConversationId(cacheKey) {
279
259
  }
280
260
  catch (err) {
281
261
  log.warn("conversation-cache:read-error", {
282
- error: err instanceof Error ? err.message : String(err),
262
+ error: commonUtils.errorMessage(err),
283
263
  });
284
264
  return undefined;
285
265
  }
@@ -301,7 +281,7 @@ async function saveCachedConversationId(cacheKey, conversationId) {
301
281
  }
302
282
  catch (err) {
303
283
  log.warn("conversation-cache:write-error", {
304
- error: err instanceof Error ? err.message : String(err),
284
+ error: commonUtils.errorMessage(err),
305
285
  });
306
286
  }
307
287
  }
@@ -314,7 +294,7 @@ async function evictCachedConversationId(cacheKey) {
314
294
  }
315
295
  catch (err) {
316
296
  log.warn("conversation-cache:delete-error", {
317
- error: err instanceof Error ? err.message : String(err),
297
+ error: commonUtils.errorMessage(err),
318
298
  });
319
299
  }
320
300
  }
@@ -397,7 +377,7 @@ function buildAskGenieTool(deps) {
397
377
  ...(seedConversationId ? { conversationId: seedConversationId } : {}),
398
378
  ...(signal ? { context: signal } : {}),
399
379
  })) {
400
- await safeWrite(writer, event);
380
+ await safeWrite(log, writer, event);
401
381
  // Wire events come in two flavors: the lifecycle `message`
402
382
  // event embeds the raw `GenieMessage` (read its
403
383
  // `conversation_id`), and the rest carry a flat
@@ -437,7 +417,7 @@ function buildAskGenieTool(deps) {
437
417
  log.warn("conversation-cache:stale, resetting", {
438
418
  spaceId,
439
419
  conversationId: seeded,
440
- error: err instanceof Error ? err.message : String(err),
420
+ error: commonUtils.errorMessage(err),
441
421
  });
442
422
  await evictCachedConversationId(cacheKey);
443
423
  writeContextConversationId(requestContext, spaceId, undefined);
@@ -663,7 +643,7 @@ export function createGenieTool(opts) {
663
643
  spaceId,
664
644
  content: input.question,
665
645
  };
666
- await safeWrite(writer, startedEvent);
646
+ await safeWrite(log, writer, startedEvent);
667
647
  const resultSets = new Map();
668
648
  // Seed the active Genie `conversation_id` onto
669
649
  // `RequestContext` from AppKit's `CacheManager` when a Mastra
@@ -801,7 +781,7 @@ export function createGenieTool(opts) {
801
781
  textItems: textItemCount,
802
782
  dataItems: dataItemCount,
803
783
  };
804
- await safeWrite(writer, summaryEvent);
784
+ await safeWrite(log, writer, summaryEvent);
805
785
  // Chart every `data` item in parallel; map `text` items to
806
786
  // the shared `string` summary variant verbatim. Missing
807
787
  // statement ids are dropped (the agent referenced something
@@ -856,7 +836,7 @@ export function createGenieTool(opts) {
856
836
  data: data.rows,
857
837
  option: planned.option,
858
838
  };
859
- await safeWrite(writer, chartEvent);
839
+ await safeWrite(log, writer, chartEvent);
860
840
  // Stash the resolved chart on the per-request
861
841
  // `RequestContext` so downstream code in the same
862
842
  // request (output processors, follow-up tool calls,
@@ -865,7 +845,7 @@ export function createGenieTool(opts) {
865
845
  recordChartInContext(requestContext, chartEvent);
866
846
  }
867
847
  catch (err) {
868
- const errorMessage = err instanceof Error ? err.message : String(err);
848
+ const errorMessage = commonUtils.errorMessage(err);
869
849
  log.warn("chart:error", {
870
850
  statementId: item.statementId,
871
851
  messageId,
@@ -883,7 +863,7 @@ export function createGenieTool(opts) {
883
863
  messageId,
884
864
  error: `chart-planner: ${errorMessage}`,
885
865
  };
886
- await safeWrite(writer, errorEvent);
866
+ await safeWrite(log, writer, errorEvent);
887
867
  }
888
868
  return {
889
869
  type: "visualize",
@@ -15,7 +15,7 @@
15
15
  * the handler runs - no cookie or user lookups happen here, and the
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
- import { logUtils } from "@dbx-tools/shared";
18
+ import { commonUtils, logUtils } from "@dbx-tools/shared";
19
19
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
20
20
  import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
21
21
  import { registerApiRoute } from "@mastra/core/server";
@@ -116,7 +116,7 @@ export async function clearHistory(opts) {
116
116
  log.debug("clear:probe-failed", {
117
117
  agentId: opts.agent.id,
118
118
  threadId: opts.threadId,
119
- error: err instanceof Error ? err.message : String(err),
119
+ error: commonUtils.errorMessage(err),
120
120
  });
121
121
  }
122
122
  const startedAt = Date.now();
@@ -131,7 +131,7 @@ export async function clearHistory(opts) {
131
131
  log.warn("clear:delete-soft-failed", {
132
132
  agentId: opts.agent.id,
133
133
  threadId: opts.threadId,
134
- error: err instanceof Error ? err.message : String(err),
134
+ error: commonUtils.errorMessage(err),
135
135
  });
136
136
  }
137
137
  log.info("clear:done", {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared helper for publishing events through Mastra's
3
+ * `ctx.writer`. Centralizes the "the downstream stream may already
4
+ * be closed, don't take the whole tool down" pattern that the
5
+ * Genie agent and chart tool both need.
6
+ *
7
+ * Failures are logged at `warn` (a persistently-closed writer is
8
+ * the most likely culprit when events go missing client-side) but
9
+ * swallowed so a cancelled request or a client that navigated
10
+ * away can't crash a tool mid-flight.
11
+ */
12
+ import type { MinimalWriter } from "@dbx-tools/appkit-mastra-shared";
13
+ import { type logUtils } from "@dbx-tools/shared";
14
+ /**
15
+ * Best-effort `writer.write`. No-op when `writer` is undefined;
16
+ * caught errors are logged via `log.warn("writer:error", ...)`
17
+ * along with any caller-supplied `context` fields (e.g. a
18
+ * `chartId` or `messageId`) so the warning is greppable per
19
+ * resource.
20
+ *
21
+ * Returns when the write resolves or rejects; never throws.
22
+ */
23
+ export declare function safeWrite(log: logUtils.Logger, writer: MinimalWriter | undefined, chunk: unknown, context?: Record<string, unknown>): Promise<void>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared helper for publishing events through Mastra's
3
+ * `ctx.writer`. Centralizes the "the downstream stream may already
4
+ * be closed, don't take the whole tool down" pattern that the
5
+ * Genie agent and chart tool both need.
6
+ *
7
+ * Failures are logged at `warn` (a persistently-closed writer is
8
+ * the most likely culprit when events go missing client-side) but
9
+ * swallowed so a cancelled request or a client that navigated
10
+ * away can't crash a tool mid-flight.
11
+ */
12
+ import { commonUtils } from "@dbx-tools/shared";
13
+ /**
14
+ * Best-effort `writer.write`. No-op when `writer` is undefined;
15
+ * caught errors are logged via `log.warn("writer:error", ...)`
16
+ * along with any caller-supplied `context` fields (e.g. a
17
+ * `chartId` or `messageId`) so the warning is greppable per
18
+ * resource.
19
+ *
20
+ * Returns when the write resolves or rejects; never throws.
21
+ */
22
+ export async function safeWrite(log, writer, chunk, context = {}) {
23
+ if (!writer) {
24
+ log.debug("writer:no-writer", context);
25
+ return;
26
+ }
27
+ try {
28
+ await writer.write(chunk);
29
+ log.debug("writer:ok", context);
30
+ }
31
+ catch (err) {
32
+ log.warn("writer:error", {
33
+ ...context,
34
+ error: commonUtils.errorMessage(err),
35
+ });
36
+ }
37
+ }