@dbx-tools/appkit-mastra 0.1.112 → 0.3.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.
@@ -0,0 +1,167 @@
1
+ import { string } from "@dbx-tools/shared-core";
2
+ /**
3
+ * Repairs Mastra / AI SDK message replays sent to Databricks Model
4
+ * Serving before they hit the OpenAI-compatible `/chat/completions`
5
+ * route.
6
+ */
7
+
8
+ /**
9
+ * OpenAI-flavoured chat message shape we need to mutate. We do not
10
+ * import the OpenAI / AI SDK types because both packages keep these
11
+ * fields under internal namespaces; the wire payload is the contract
12
+ * here and it's stable enough to inline.
13
+ */
14
+ export interface ServingChatMessage {
15
+ role: "system" | "user" | "assistant" | "tool" | "reasoning";
16
+ content?: string | ServingContentPart[];
17
+ tool_calls?: Array<{ id: string; type: string; function: unknown }>;
18
+ tool_call_id?: string;
19
+ reasoning?: unknown;
20
+ reasoning_content?: unknown;
21
+ }
22
+
23
+ type ServingContentPart = {
24
+ type?: string;
25
+ text?: string;
26
+ [key: string]: unknown;
27
+ };
28
+
29
+ const REASONING_PART_TYPES = new Set(["reasoning", "thinking", "redacted_thinking"]);
30
+
31
+ /**
32
+ * Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
33
+ * body. Returns the original string verbatim when the body is not
34
+ * JSON, has no `messages`, or no rewrite was needed.
35
+ */
36
+ export function rewriteServingBody(body: string): string {
37
+ let parsed: { messages?: unknown };
38
+ try {
39
+ parsed = JSON.parse(body);
40
+ } catch {
41
+ return body;
42
+ }
43
+ if (!Array.isArray(parsed.messages)) return body;
44
+ const messages = parsed.messages as ServingChatMessage[];
45
+ const changed = stripReasoningFromServingMessages(messages) || repairAssistantPrefill(messages);
46
+ return changed ? JSON.stringify(parsed) : body;
47
+ }
48
+
49
+ /**
50
+ * Drop extended-thinking / reasoning blocks from a replayed transcript.
51
+ *
52
+ * Hybrid Claude endpoints (e.g. Sonnet 4.5+) may emit `reasoning` content
53
+ * parts on the first turn. Mastra persists and replays them on the next
54
+ * agent step, but Databricks-hosted Claude rejects those blocks on
55
+ * multi-turn tool continuations. The UI already captured reasoning for
56
+ * display; stripping here keeps provider replay compatible without
57
+ * changing what users see in the chat bubble.
58
+ */
59
+ export function stripReasoningFromServingMessages(messages: ServingChatMessage[]): boolean {
60
+ let changed = false;
61
+ for (let i = messages.length - 1; i >= 0; i--) {
62
+ const msg = messages[i];
63
+ if (!msg) continue;
64
+ if (msg.role === "reasoning") {
65
+ messages.splice(i, 1);
66
+ changed = true;
67
+ continue;
68
+ }
69
+ if (msg.reasoning !== undefined) {
70
+ delete msg.reasoning;
71
+ changed = true;
72
+ }
73
+ if (msg.reasoning_content !== undefined) {
74
+ delete msg.reasoning_content;
75
+ changed = true;
76
+ }
77
+ if (!Array.isArray(msg.content)) continue;
78
+ const filtered = msg.content.filter((part) => {
79
+ const type = part?.type;
80
+ if (typeof type === "string" && REASONING_PART_TYPES.has(type)) {
81
+ changed = true;
82
+ return false;
83
+ }
84
+ return true;
85
+ });
86
+ if (filtered.length !== msg.content.length) {
87
+ msg.content = filtered;
88
+ }
89
+ const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
90
+ if (msg.role === "assistant" && !hasToolCalls && isEmptyServingContent(msg.content)) {
91
+ messages.splice(i, 1);
92
+ changed = true;
93
+ }
94
+ }
95
+ return changed;
96
+ }
97
+
98
+ /**
99
+ * Repair a Mastra/AI SDK message replay that Databricks-hosted Claude
100
+ * rejects with `"This model does not support assistant message
101
+ * prefill. The conversation must end with a user message."`.
102
+ *
103
+ * The bug pattern: when an assistant turn streams text *and* a
104
+ * `tool_call`, the AI SDK persists them as two separate assistant
105
+ * entries (text-only and tool-call-only). On the next agent step the
106
+ * tool-call entry is replayed *before* the tool result and the
107
+ * text entry is replayed *after* it, so the conversation ends with a
108
+ * trailing assistant text message. Anthropic interprets that as a
109
+ * prefill request and rejects it on Databricks (the upstream Bedrock
110
+ * route disallows prefill).
111
+ *
112
+ * Fix: when the last message is an assistant text with no `tool_calls`
113
+ * and the chain immediately before it is `assistant(tool_calls=...)`
114
+ * followed only by `tool(...)` results, fold the trailing text back
115
+ * into the `content` of that opening assistant and drop the duplicate.
116
+ */
117
+ export function repairAssistantPrefill(messages: ServingChatMessage[]): boolean {
118
+ if (messages.length < 2) return false;
119
+ const last = messages[messages.length - 1];
120
+ if (!last || last.role !== "assistant" || (last.tool_calls && last.tool_calls.length > 0)) {
121
+ return false;
122
+ }
123
+
124
+ let i = messages.length - 2;
125
+ while (i >= 0 && messages[i]?.role === "tool") i--;
126
+ if (i < 0) return false;
127
+ const opener = messages[i];
128
+ if (
129
+ !opener ||
130
+ opener.role !== "assistant" ||
131
+ !opener.tool_calls ||
132
+ opener.tool_calls.length === 0
133
+ ) {
134
+ return false;
135
+ }
136
+
137
+ const merged = [
138
+ string.trimToNull(textFromServingContent(opener.content)),
139
+ string.trimToNull(textFromServingContent(last.content)),
140
+ ]
141
+ .filter((s): s is string => s !== null)
142
+ .join("\n\n");
143
+ opener.content = merged;
144
+ messages.pop();
145
+ return true;
146
+ }
147
+
148
+ function textFromServingContent(content: ServingChatMessage["content"]): string {
149
+ if (typeof content === "string") return content;
150
+ if (!Array.isArray(content)) return "";
151
+ return content
152
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
153
+ .map((part) => part.text as string)
154
+ .join("\n\n");
155
+ }
156
+
157
+ function isEmptyServingContent(content: ServingChatMessage["content"]): boolean {
158
+ if (content === undefined) return true;
159
+ if (typeof content === "string") return content.trim().length === 0;
160
+ if (!Array.isArray(content)) return true;
161
+ return content.every((part) => {
162
+ if (part?.type === "text") {
163
+ return typeof part.text !== "string" || part.text.trim().length === 0;
164
+ }
165
+ return false;
166
+ });
167
+ }
package/src/serving.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
3
+ *
4
+ * The live `/serving-endpoints` catalogue access, fuzzy name
5
+ * resolution, and class/fallback selection all live in
6
+ * `@dbx-tools/model`; this module only adds what is specific to the
7
+ * Mastra plugin: pulling a per-request model override off an HTTP
8
+ * request (header / query / body) and projecting the plugin config
9
+ * onto the knobs `buildModel` and the `/models` route share.
10
+ */
11
+
12
+ import { override } from "@dbx-tools/shared-mastra";
13
+ import { serving as nodeServing } from "@dbx-tools/model";
14
+
15
+ import type { MastraPluginConfig } from "./config";
16
+ import { string } from "@dbx-tools/shared-core";
17
+
18
+ /**
19
+ * `RequestContext` key under which {@link MastraServer} stores the
20
+ * per-request model override (header / query / body). `model.ts`
21
+ * reads it before falling back to the agent / plugin default.
22
+ */
23
+ export const MASTRA_MODEL_OVERRIDE_KEY = "mastra__model_override";
24
+
25
+ /**
26
+ * Minimal Express-ish request shape used by {@link extractModelOverride}.
27
+ * Keeps this module independent of `express` so the helper can be
28
+ * reused from non-Express adapters.
29
+ */
30
+ export interface ModelOverrideRequest {
31
+ headers?: Record<string, string | string[] | undefined>;
32
+ query?: Record<string, unknown> | undefined;
33
+ body?: unknown;
34
+ }
35
+
36
+ /**
37
+ * Pull a model override out of a single HTTP request, checking
38
+ * sources in priority order:
39
+ *
40
+ * 1. `X-Mastra-Model` header
41
+ * 2. `?model=` query string parameter
42
+ * 3. Body field (`model` or `modelId`, in that order)
43
+ *
44
+ * Returns `null` when nothing is set, so callers can wrap with
45
+ * `if (override) ...` without juggling empty strings. Body inspection
46
+ * is lenient - any plain object with one of the configured keys
47
+ * counts, mirroring how AI SDK chat clients pass arbitrary metadata
48
+ * alongside `messages`.
49
+ */
50
+ export function extractModelOverride(req: ModelOverrideRequest): string | null {
51
+ const headers = req.headers;
52
+ if (headers) {
53
+ const headerVal = string.firstNonEmpty(
54
+ headers[override.MODEL_OVERRIDE_HEADER] ??
55
+ headers[override.MODEL_OVERRIDE_HEADER.toLowerCase()],
56
+ );
57
+ if (headerVal) return headerVal;
58
+ }
59
+ if (req.query) {
60
+ const queryVal = string.firstNonEmpty(req.query[override.MODEL_OVERRIDE_QUERY]);
61
+ if (queryVal) return queryVal;
62
+ }
63
+ if (req.body && typeof req.body === "object") {
64
+ const record = req.body as Record<string, unknown>;
65
+ for (const field of override.MODEL_OVERRIDE_BODY_FIELDS) {
66
+ const bodyVal = string.firstNonEmpty(record[field]);
67
+ if (bodyVal) return bodyVal;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Read the fuzzy-resolution config knobs off the plugin config with
75
+ * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
76
+ * the `/models` route agree on what "enabled" means.
77
+ *
78
+ * `fallbacks` is the priority-ordered list `resolveModel` walks first
79
+ * when nothing explicit is set; defaults to an empty list so the
80
+ * generic resolver falls through to the live catalogue and its own
81
+ * `FALLBACK_MODEL_IDS` floor.
82
+ */
83
+ export function resolveServingConfig(config: MastraPluginConfig): {
84
+ ttlMs: number;
85
+ threshold: number;
86
+ fuzzy: boolean;
87
+ allowOverride: boolean;
88
+ fallbacks: readonly string[];
89
+ } {
90
+ return {
91
+ ttlMs: config.modelCacheTtlMs ?? nodeServing.DEFAULT_MODEL_CACHE_TTL_MS,
92
+ threshold: config.modelFuzzyThreshold ?? nodeServing.DEFAULT_FUZZY_THRESHOLD,
93
+ fuzzy: config.modelFuzzyMatch !== false,
94
+ allowOverride: config.modelOverride !== false,
95
+ fallbacks: config.defaultModelFallbacks ?? [],
96
+ };
97
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Databricks Statement Execution helpers for the Mastra plugin.
3
+ *
4
+ * Wraps `client.statementExecution.getStatement` with the shape and
5
+ * size handling the plugin's tools and the `/embed/data/:id` route
6
+ * both need: a low-level fetch that returns a raw
7
+ * `{columns, rows, rowCount}` shape and coerces numeric strings to
8
+ * numbers so downstream charts and aggregations don't have to, plus a
9
+ * hard row cap callers clamp `limit` to so a runaway result set can't
10
+ * hose a response. Upstream 404s are detected via
11
+ * `error.errorContext(err).notFound` at the call sites.
12
+ *
13
+ * Not Genie-specific: a Databricks `statement_id` is workspace
14
+ * scoped and lives in the Statement Execution API regardless of
15
+ * which producer (Genie, a tool, a notebook, etc.) submitted the
16
+ * query. Co-located here so consumers can fetch / cap / handle
17
+ * 404s without reaching into the Genie tool module.
18
+ */
19
+
20
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
21
+ import type { GenieDatasetData } from "@dbx-tools/shared-mastra";
22
+ import { databricks } from "@dbx-tools/appkit";
23
+
24
+ /**
25
+ * Hard server-side cap on rows returned by the
26
+ * `/embed/data/:id` route. Sized to keep responses small
27
+ * enough for inline tables to render snappily; the route surfaces
28
+ * a `truncated` flag whenever the upstream `rowCount` exceeds
29
+ * this so end users know they're seeing a sample.
30
+ */
31
+ export const STATEMENT_ROW_CAP = 500;
32
+
33
+ /**
34
+ * Best-effort numeric coercion for the Statement Execution API's
35
+ * all-strings cells. Leaves non-numeric strings (and explicit
36
+ * `null`s) intact; everything else flows through `Number`.
37
+ */
38
+ function coerceCell(cell: string | null): unknown {
39
+ if (cell === null) return null;
40
+ if (/^-?\d+(\.\d+)?$/.test(cell)) {
41
+ const n = Number(cell);
42
+ if (Number.isFinite(n)) return n;
43
+ }
44
+ return cell;
45
+ }
46
+
47
+ /**
48
+ * Fetch a single statement's rows via the Statement Execution API
49
+ * and reshape into the shared {@link GenieDatasetData} shape
50
+ * (column array + row records).
51
+ *
52
+ * Optional `limit` slices the returned `rows` client-side so the
53
+ * agent can scan a small sample without paging the full result
54
+ * set into context. `rowCount` always reflects the upstream total
55
+ * so callers know when the slice truncated.
56
+ *
57
+ * Exported because every consumer in the plugin (the
58
+ * `get_statement` tool, the `prepare_chart` dataset resolver, and
59
+ * the `/embed/data/:id` route) needs the exact same
60
+ * fetch + coercion pipeline so LLM-side `get_statement` output
61
+ * and UI-side `[data:<id>]` rendering stay shape-identical for
62
+ * the same `statement_id`.
63
+ */
64
+ export async function fetchStatementData(
65
+ client: WorkspaceClient,
66
+ statementId: string,
67
+ options?: { limit?: number; signal?: AbortSignal },
68
+ ): Promise<GenieDatasetData> {
69
+ const ctx = options?.signal ? databricks.toContext(options.signal) : undefined;
70
+ const r = await client.statementExecution.getStatement({ statement_id: statementId }, ctx);
71
+ const columns = (r.manifest?.schema?.columns ?? []).map((c) => c.name ?? "");
72
+ const dataArray = (r.result?.data_array ?? []) as Array<Array<string | null>>;
73
+ const sliced =
74
+ options?.limit !== undefined && options.limit >= 0
75
+ ? dataArray.slice(0, options.limit)
76
+ : dataArray;
77
+ const rows = sliced.map((row) => {
78
+ const obj: Record<string, unknown> = {};
79
+ columns.forEach((col, i) => {
80
+ obj[col] = coerceCell(row[i] ?? null);
81
+ });
82
+ return obj;
83
+ });
84
+ return {
85
+ columns,
86
+ rows,
87
+ rowCount: r.manifest?.total_row_count ?? dataArray.length,
88
+ };
89
+ }
@@ -0,0 +1,41 @@
1
+ import { string } from "@dbx-tools/shared-core";
2
+ /**
3
+ * Derive a Postgres-safe schema name for per-agent Mastra storage.
4
+ *
5
+ * Agent ids are often kebab-case route segments (`data-mesh-book-assistant`)
6
+ * but {@link PostgresStore} validates `schemaName` with Mastra's
7
+ * `parseSqlIdentifier` (letter/underscore start, `[A-Za-z0-9_]` only, max 63).
8
+ */
9
+
10
+ const SCHEMA_PREFIX = "mastra_";
11
+ const MAX_PG_IDENTIFIER_LEN = 63;
12
+
13
+ /**
14
+ * Default Lakebase schema for one agent's thread/message store:
15
+ * `mastra_<sanitized-agent-id>`.
16
+ */
17
+ export function agentStorageSchemaName(agentId: string): string {
18
+ const maxSlugLen = MAX_PG_IDENTIFIER_LEN - SCHEMA_PREFIX.length;
19
+ const slug = string.toIdentifierWithOptions(
20
+ {
21
+ delimiter: "_",
22
+ maxLength: maxSlugLen,
23
+ truncateStrategy: "hash",
24
+ truncateHashLength: 6,
25
+ },
26
+ agentId,
27
+ );
28
+ const body =
29
+ slug ||
30
+ string.toIdentifierWithOptions(
31
+ {
32
+ delimiter: "_",
33
+ maxLength: maxSlugLen,
34
+ truncateStrategy: "hash",
35
+ truncateHashLength: 6,
36
+ },
37
+ "agent",
38
+ agentId,
39
+ );
40
+ return `${SCHEMA_PREFIX}${body}`;
41
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Small-tier summarization for the Mastra plugin.
3
+ *
4
+ * Two surfaces, both backed by the fast / small chat tier
5
+ * ({@link model.ModelClass.ChatFast}) resolved through the same
6
+ * `/serving-endpoints` pipeline as the main agents:
7
+ *
8
+ * - A dedicated `summarize` tool (see {@link buildSummarizeTool})
9
+ * agents can call to condense arbitrary text without burning the
10
+ * heavyweight chat model.
11
+ * - The model + instructions Mastra's memory uses to auto-name
12
+ * conversation threads (`generateTitle`), so titling reuses the
13
+ * same small tier rather than the agent's primary model.
14
+ *
15
+ * Mirrors the chart-planner wiring in `chart.ts`: a per-config cached
16
+ * `Agent` on the fast tier, invoked via `agent.generate(...)` inside
17
+ * the active `asUser` scope so tokens stay user-scoped.
18
+ */
19
+
20
+ import { model } from "@dbx-tools/shared-model";
21
+ import { Agent } from "@mastra/core/agent";
22
+ import type { MastraModelConfig } from "@mastra/core/llm";
23
+ import type { RequestContext } from "@mastra/core/request-context";
24
+ import { createTool } from "@mastra/core/tools";
25
+ import { z } from "zod";
26
+
27
+ import type { MastraPluginConfig } from "./config";
28
+ import { buildModel } from "./model";
29
+ import { string } from "@dbx-tools/shared-core";
30
+
31
+ /** Fast / small chat tier used for both titling and summaries. */
32
+ const SUMMARY_MODEL_CLASS = model.ModelClass.ChatFast;
33
+
34
+ /** System prompt for the summarizer agent (and the `summarize` tool). */
35
+ const SUMMARIZER_INSTRUCTIONS = [
36
+ "You are a summarization engine.",
37
+ "Given a block of text, produce a faithful, concise summary of it.",
38
+ "Default to a few sentences; follow any length guidance the caller gives.",
39
+ "Plain prose. No preamble, no headers, no bullet points unless asked.",
40
+ "Never use emojis. Use hyphens (-) only, never em dashes or en dashes.",
41
+ "Never add information, opinions, or details not present in the input.",
42
+ "Output only the summary.",
43
+ ].join("\n");
44
+
45
+ /**
46
+ * Instructions Mastra's `generateTitle` hands the small-tier model to
47
+ * name a conversation thread from its opening turn. Kept terse so the
48
+ * model returns a bare title with no decoration.
49
+ */
50
+ export const TITLE_INSTRUCTIONS = [
51
+ "Generate a short, specific title for this conversation, 3 to 6 words.",
52
+ "Capture the user's topic, not the assistant's response.",
53
+ "Plain text only: no surrounding quotes, no trailing punctuation,",
54
+ "no emojis, and no em dashes. Output only the title.",
55
+ ].join(" ");
56
+
57
+ /**
58
+ * Resolve the small-tier model for summarization / titling. Reused by
59
+ * both the summarizer agent and Mastra memory's `generateTitle`.
60
+ *
61
+ * Returned as a `requestContext`-taking function (a Mastra
62
+ * `DynamicArgument<MastraModelConfig>`) so each call mints user-scoped
63
+ * tokens via {@link buildModel}, exactly like the primary agents.
64
+ */
65
+ export function summaryModel(
66
+ config: MastraPluginConfig,
67
+ ): (args: { requestContext: RequestContext }) => Promise<MastraModelConfig> {
68
+ return ({ requestContext }) =>
69
+ buildModel(config, requestContext, { modelClass: SUMMARY_MODEL_CLASS });
70
+ }
71
+
72
+ /**
73
+ * One summarizer `Agent` per plugin config, cached on config-object
74
+ * identity so a hot tool path doesn't pay the constructor cost each
75
+ * call. `WeakMap` lets retired configs (e.g. test reconfigurations)
76
+ * release their agent without manual eviction.
77
+ */
78
+ const summarizerAgents = new WeakMap<MastraPluginConfig, Agent>();
79
+
80
+ function getSummarizerAgent(config: MastraPluginConfig): Agent {
81
+ let agent = summarizerAgents.get(config);
82
+ if (!agent) {
83
+ agent = new Agent({
84
+ id: "summarizer",
85
+ name: "Summarizer",
86
+ description: "Condenses text into a short summary using a fast, small model.",
87
+ instructions: SUMMARIZER_INSTRUCTIONS,
88
+ model: summaryModel(config),
89
+ });
90
+ summarizerAgents.set(config, agent);
91
+ }
92
+ return agent;
93
+ }
94
+
95
+ /** Inputs accepted by the `summarize` tool. */
96
+ const summarizeInput = z.object({
97
+ text: z.string().min(1).describe("The text to summarize."),
98
+ instructions: z
99
+ .string()
100
+ .optional()
101
+ .describe(
102
+ "Optional extra guidance for the summary, e.g. 'one sentence' or 'list the action items'.",
103
+ ),
104
+ maxWords: z
105
+ .number()
106
+ .int()
107
+ .positive()
108
+ .optional()
109
+ .describe("Optional soft cap on the summary length, in words."),
110
+ });
111
+
112
+ /** Options accepted by {@link summarizeText}. */
113
+ export interface SummarizeOptions {
114
+ /** Extra guidance for the summary (length, focus, format). */
115
+ instructions?: string;
116
+ /** Soft cap on summary length, in words. */
117
+ maxWords?: number;
118
+ /** Active request context, so the model resolver mints user-scoped tokens. */
119
+ requestContext?: RequestContext;
120
+ /** Abort signal bridged from the calling tool / request. */
121
+ abortSignal?: AbortSignal;
122
+ }
123
+
124
+ /**
125
+ * Summarize `text` with the small-tier summarizer agent, returning the
126
+ * trimmed summary string. Throws on model failure (the caller decides
127
+ * how to degrade).
128
+ */
129
+ export async function summarizeText(
130
+ config: MastraPluginConfig,
131
+ text: string,
132
+ options: SummarizeOptions = {},
133
+ ): Promise<string> {
134
+ const { instructions, maxWords, requestContext, abortSignal } = options;
135
+ const prompt = string.toDescription({
136
+ ...(instructions ? { Guidance: instructions } : {}),
137
+ ...(maxWords !== undefined ? { "Max length (words)": String(maxWords) } : {}),
138
+ Text: text,
139
+ });
140
+ const result = await getSummarizerAgent(config).generate(prompt, {
141
+ ...(requestContext ? { requestContext } : {}),
142
+ ...(abortSignal ? { abortSignal } : {}),
143
+ });
144
+ return result.text.trim();
145
+ }
146
+
147
+ /**
148
+ * Build the `summarize` tool. Exposed as an ambient system tool (like
149
+ * `render_data`) so every agent can offload condensing long content,
150
+ * notes, transcripts, or bulky tool results to the fast tier instead of
151
+ * spending its primary chat model. The tool reads the live
152
+ * `requestContext` / `abortSignal` off the Mastra execution context so
153
+ * its model call stays user-scoped and cancels with the turn.
154
+ */
155
+ export function buildSummarizeTool(config: MastraPluginConfig) {
156
+ return createTool({
157
+ id: "summarize",
158
+ description:
159
+ "Summarize a block of text using a fast, small model. Use it to condense long " +
160
+ "content, notes, transcripts, or tool results into a short summary without " +
161
+ "spending the main chat model.",
162
+ inputSchema: summarizeInput,
163
+ execute: async (input, context) => {
164
+ const { text, instructions, maxWords } = input as z.infer<typeof summarizeInput>;
165
+ const ctx = context as
166
+ { requestContext?: RequestContext; abortSignal?: AbortSignal } | undefined;
167
+ const summary = await summarizeText(config, text, {
168
+ ...(instructions ? { instructions } : {}),
169
+ ...(maxWords !== undefined ? { maxWords } : {}),
170
+ ...(ctx?.requestContext ? { requestContext: ctx.requestContext } : {}),
171
+ ...(ctx?.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
172
+ });
173
+ return { summary };
174
+ },
175
+ });
176
+ }