better-mastra 0.1.1 → 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.
Files changed (80) hide show
  1. package/README.md +35 -0
  2. package/dist/client/actions.d.ts +21 -3
  3. package/dist/client/actions.d.ts.map +1 -1
  4. package/dist/client/actions.js +30 -5
  5. package/dist/client/actions.js.map +1 -1
  6. package/dist/client/messages.d.ts +18 -3
  7. package/dist/client/messages.d.ts.map +1 -1
  8. package/dist/client/messages.js +24 -14
  9. package/dist/client/messages.js.map +1 -1
  10. package/dist/client/reveal.d.ts +2 -0
  11. package/dist/client/reveal.d.ts.map +1 -1
  12. package/dist/client/reveal.js +5 -0
  13. package/dist/client/reveal.js.map +1 -1
  14. package/dist/client/session.d.ts +8 -7
  15. package/dist/client/session.d.ts.map +1 -1
  16. package/dist/client/session.js +1 -0
  17. package/dist/client/session.js.map +1 -1
  18. package/dist/client/turn.d.ts +2 -2
  19. package/dist/client/turn.d.ts.map +1 -1
  20. package/dist/client/turn.js.map +1 -1
  21. package/dist/hono/chatRoute.d.ts +10 -1
  22. package/dist/hono/chatRoute.d.ts.map +1 -1
  23. package/dist/hono/chatRoute.js +5 -4
  24. package/dist/hono/chatRoute.js.map +1 -1
  25. package/dist/hono/index.d.ts +1 -1
  26. package/dist/hono/index.d.ts.map +1 -1
  27. package/dist/hono/index.js.map +1 -1
  28. package/dist/libsql/index.d.ts +7 -0
  29. package/dist/libsql/index.d.ts.map +1 -0
  30. package/dist/libsql/index.js +13 -0
  31. package/dist/libsql/index.js.map +1 -0
  32. package/dist/models/providers/openrouter.d.ts +5 -3
  33. package/dist/models/providers/openrouter.d.ts.map +1 -1
  34. package/dist/models/providers/openrouter.js +11 -6
  35. package/dist/models/providers/openrouter.js.map +1 -1
  36. package/dist/openrouter/index.d.ts +2 -0
  37. package/dist/openrouter/index.d.ts.map +1 -0
  38. package/dist/openrouter/index.js +2 -0
  39. package/dist/openrouter/index.js.map +1 -0
  40. package/dist/pg/index.d.ts +11 -0
  41. package/dist/pg/index.d.ts.map +1 -0
  42. package/dist/pg/index.js +85 -0
  43. package/dist/pg/index.js.map +1 -0
  44. package/dist/react/markdown/index.d.ts +2 -0
  45. package/dist/react/markdown/index.d.ts.map +1 -1
  46. package/dist/react/markdown/index.js +6 -3
  47. package/dist/react/markdown/index.js.map +1 -1
  48. package/dist/resumable/index.d.ts +21 -2
  49. package/dist/resumable/index.d.ts.map +1 -1
  50. package/dist/resumable/index.js +17 -8
  51. package/dist/resumable/index.js.map +1 -1
  52. package/dist/resumable/redis.d.ts +29 -0
  53. package/dist/resumable/redis.d.ts.map +1 -1
  54. package/dist/resumable/redis.js +60 -0
  55. package/dist/resumable/redis.js.map +1 -1
  56. package/dist/runtime/storage.d.ts +8 -6
  57. package/dist/runtime/storage.d.ts.map +1 -1
  58. package/dist/runtime/storage.js +9 -89
  59. package/dist/runtime/storage.js.map +1 -1
  60. package/dist/turn/threads.d.ts +7 -1
  61. package/dist/turn/threads.d.ts.map +1 -1
  62. package/dist/turn/threads.js +5 -32
  63. package/dist/turn/threads.js.map +1 -1
  64. package/package.json +14 -1
  65. package/src/client/actions.ts +72 -7
  66. package/src/client/messages.ts +44 -16
  67. package/src/client/reveal.ts +6 -0
  68. package/src/client/session.ts +15 -9
  69. package/src/client/turn.ts +2 -2
  70. package/src/hono/chatRoute.ts +17 -5
  71. package/src/hono/index.ts +5 -1
  72. package/src/libsql/index.ts +19 -0
  73. package/src/models/providers/openrouter.ts +19 -7
  74. package/src/openrouter/index.ts +1 -0
  75. package/src/pg/index.ts +108 -0
  76. package/src/react/markdown/index.ts +9 -5
  77. package/src/resumable/index.ts +47 -18
  78. package/src/resumable/redis.ts +91 -0
  79. package/src/runtime/storage.ts +10 -106
  80. package/src/turn/threads.ts +20 -35
package/README.md CHANGED
@@ -4,6 +4,26 @@ A small layer on Mastra and the AI SDK: model registry, chat turns, resumable st
4
4
 
5
5
  Work in progress.
6
6
 
7
+ ## Runtime presets
8
+
9
+ Storage and memory presets are synchronous; pools connect on their first query, so a process can build its Mastra at import time:
10
+
11
+ ```ts
12
+ import { Mastra } from '@mastra/core'
13
+ import { createMemory, shutdown } from 'better-mastra' // presets come from the package root
14
+ import { openRouterRoute } from 'better-mastra/openrouter'
15
+ import { pgStorage } from 'better-mastra/pg' // or libsqlStorage from 'better-mastra/libsql'
16
+
17
+ const storage = pgStorage({ connectionString: process.env.DATABASE_URL })
18
+ const models = openRouterRoute({ apiKey: process.env.OPENROUTER_API_KEY })
19
+ const memory = createMemory({ storage: storage.store, observer: models.model('google/gemini-3.5-flash') })
20
+ export const mastra = new Mastra({ storage: storage.store })
21
+
22
+ await storage.init() // once, in the process that migrates
23
+ ```
24
+
25
+ `pgStorage` and `libsqlStorage` from the package root are the same presets loaded on call (they return promises); they stay for compatibility.
26
+
7
27
  ## Resumable streams (`better-mastra/resumable`)
8
28
 
9
29
  Opt-in. A dropped connection no longer aborts the turn: the client reconnects with AI SDK `reconnectToStream` (`useChat({ resume: true })`) and gets a full replay of the current assistant message plus the live tail (`resumeStreamIsReplay: true`, the `DefaultChatTransport` default).
@@ -31,6 +51,21 @@ await resumable.close()
31
51
 
32
52
  Use `ioRedis(client)` for ioredis. Both adapters open extra connections with `duplicate()`: one subscriber per replica, and one per cross-replica reader (for `XREAD BLOCK`).
33
53
 
54
+ Use `upstashRedis(client)` for `@upstash/redis`. Upstash has no blocking `XREAD`, so a cross-replica reader polls every `pollMs` (200 ms); pass `() => Redis.fromEnv()` to create the client on first use.
55
+
56
+ An app that already holds a per-thread lock can keep the locator in the lock's value with `locator: { read, write, expire }`. The locator then lives exactly as long as the lock. `expire` renews it while the turn runs and shortens it to `graceMs` after; leave it out when the lock renews itself:
57
+
58
+ ```ts
59
+ createResumableStreams({
60
+ redis: upstashRedis(() => Redis.fromEnv()),
61
+ locator: {
62
+ read: (threadId) => readLockValue(threadId), // what follows the lock token
63
+ write: (threadId, locator, ttlMs) => writeLockValue(threadId, locator, ttlMs),
64
+ expire: (threadId, ttlMs) => redis.pexpire(lockKey(threadId), ttlMs)
65
+ }
66
+ })
67
+ ```
68
+
34
69
  How it works: chunks are buffered in memory (consecutive `text-delta`s merged) and fanned out to subscribers. A per-thread locator `{ streamId, owner }` in Redis says which replica owns the turn. Reconnecting to the owner is served from memory. Reconnecting to another replica publishes a spill request on the owner's control channel; the owner writes its buffer in one `XADD`, then mirrors new chunks in 50 ms batches (first one immediate) until a terminal marker, and the reader follows with `XREAD BLOCK`.
35
70
 
36
71
  Redis ops for a ~1,500-chunk turn:
@@ -6,6 +6,15 @@
6
6
  */
7
7
  import type { UIMessage } from 'ai';
8
8
  import { z } from 'zod';
9
+ type MessagePart = UIMessage['parts'][number];
10
+ /** Where the call sits, for an action with its own policy on repeats or shared steps. */
11
+ export interface ClientActionContext {
12
+ readonly toolCallId: string;
13
+ /** The chat's messages when the call arrived; the call is in the last one. */
14
+ readonly messages: readonly UIMessage[];
15
+ /** The parts of the model step that made the call, the call and any server tool beside it included. */
16
+ readonly step: readonly MessagePart[];
17
+ }
9
18
  /** A tool that runs in the client. The server never interprets its name. */
10
19
  export interface ClientAction<TName extends string = string> {
11
20
  readonly name: TName;
@@ -13,7 +22,13 @@ export interface ClientAction<TName extends string = string> {
13
22
  readonly inputSchema: z.ZodType;
14
23
  readonly requiresApproval?: boolean;
15
24
  /** Runs the action on the model's raw input. Throw to report a failure: the model reads it as an error it can act on. */
16
- readonly run: (input: unknown) => Promise<unknown>;
25
+ readonly run: (input: unknown, context: ClientActionContext) => Promise<unknown>;
26
+ /**
27
+ * A call the user turn already made with the same input. Its result is
28
+ * posted as the call's output; throw to post an error. Default: an error
29
+ * with `CLIENT_ACTION_REPEATED`.
30
+ */
31
+ readonly onRepeat?: (input: unknown, context: ClientActionContext) => Promise<unknown>;
17
32
  }
18
33
  /** A client action as a request carries it, for the server to offer the model. */
19
34
  export interface ClientToolDef {
@@ -60,7 +75,8 @@ export declare function defineClientAction<const TName extends string, TSchema e
60
75
  readonly inputSchema: TSchema;
61
76
  readonly requiresApproval?: boolean;
62
77
  readonly resultSummary?: string;
63
- readonly handler: (input: z.output<TSchema>) => Promise<TOutput>;
78
+ readonly handler: (input: z.output<TSchema>, context: ClientActionContext) => Promise<TOutput>;
79
+ readonly onRepeat?: ClientAction['onRepeat'];
64
80
  }): ClientAction<TName>;
65
81
  /**
66
82
  * Model arguments with `null` taken out wherever the schema means "may be
@@ -76,10 +92,12 @@ export declare function toClientToolDefs(actions: readonly ClientAction[]): Clie
76
92
  * Carries out the client action a tool call names and posts its result on the
77
93
  * chat; null when no action has that name (a server tool: its result arrives
78
94
  * on the stream). A call the user turn already made with the same input
79
- * (`null`s aside) is refused instead of carried out again.
95
+ * (`null`s aside) is refused instead of carried out again, unless the action
96
+ * has its own `onRepeat`.
80
97
  *
81
98
  * Never await it inside the chat's `onToolCall`: the AI SDK runs that callback
82
99
  * on the queue `addToolOutput` waits on, so awaiting deadlocks the stream.
83
100
  */
84
101
  export declare function runClientAction(chat: ClientActionChat, actions: readonly ClientAction[], toolCall: ClientToolCall): Promise<void> | null;
102
+ export {};
85
103
  //# sourceMappingURL=actions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/client/actions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AAEnC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,4EAA4E;AAC5E,MAAM,WAAW,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IAC1D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAA;IAC/B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IACnC,yHAAyH;IACzH,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAClD;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACvB;AAED,2DAA2D;AAC3D,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAA;IAC9B,QAAQ,CAAC,aAAa,EAAE,CACvB,OAAO,EACJ;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,GACrD;QACA,KAAK,EAAE,cAAc,CAAA;QACrB,IAAI,EAAE,MAAM,CAAA;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,SAAS,EAAE,MAAM,CAAA;KAChB,KACA,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;CAC7B,CAAA;AAcD,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAExD,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,8FACoD,CAAA;AAE5F,wFAAwF;AACxF,eAAO,MAAM,sBAAsB,mGAC8D,CAAA;AAIjG;;;;;GAKG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,CAAC,KAAK,SAAS,MAAM,EAC1B,OAAO,SAAS,CAAC,CAAC,OAAO,EACzB,OAAO,EACN,MAAM,EAAE;IACT,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,GAAG,YAAY,CAAC,KAAK,CAAC,CAwBtB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,CAAC,CAAC,OAAO,EACjB,KAAK,EAAE,OAAO,GACZ,OAAO,CA+ET;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,SAAS,YAAY,EAAE,GAC9B,aAAa,EAAE,CAYjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,QAAQ,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAoCtB"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/client/actions.ts"],"names":[],"mappings":"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"}
@@ -29,12 +29,13 @@ export function defineClientAction(action) {
29
29
  ...(action.requiresApproval === undefined
30
30
  ? {}
31
31
  : { requiresApproval: action.requiresApproval }),
32
- run: async (input) => {
32
+ ...(action.onRepeat === undefined ? {} : { onRepeat: action.onRepeat }),
33
+ run: async (input, context) => {
33
34
  const parsed = action.inputSchema.safeParse(normalizeModelNulls(action.inputSchema, input ?? {}));
34
35
  if (!parsed.success) {
35
36
  throw new Error(CLIENT_ACTION_INVALID_INPUT);
36
37
  }
37
- const data = await action.handler(parsed.data);
38
+ const data = await action.handler(parsed.data, context);
38
39
  return action.resultSummary === undefined
39
40
  ? data
40
41
  : { summary: action.resultSummary, data };
@@ -128,7 +129,8 @@ export function toClientToolDefs(actions) {
128
129
  * Carries out the client action a tool call names and posts its result on the
129
130
  * chat; null when no action has that name (a server tool: its result arrives
130
131
  * on the stream). A call the user turn already made with the same input
131
- * (`null`s aside) is refused instead of carried out again.
132
+ * (`null`s aside) is refused instead of carried out again, unless the action
133
+ * has its own `onRepeat`.
132
134
  *
133
135
  * Never await it inside the chat's `onToolCall`: the AI SDK runs that callback
134
136
  * on the queue `addToolOutput` waits on, so awaiting deadlocks the stream.
@@ -144,13 +146,21 @@ export function runClientAction(chat, actions, toolCall) {
144
146
  toolCallId: toolCall.toolCallId,
145
147
  errorText
146
148
  });
149
+ const context = {
150
+ toolCallId: toolCall.toolCallId,
151
+ messages: chat.messages,
152
+ step: stepOf(chat.messages, toolCall.toolCallId)
153
+ };
147
154
  return (async () => {
148
- if (isRepeatInTurn(chat.messages, action, toolCall)) {
155
+ const repeated = isRepeatInTurn(chat.messages, action, toolCall);
156
+ if (repeated && action.onRepeat === undefined) {
149
157
  await fail(CLIENT_ACTION_REPEATED);
150
158
  return;
151
159
  }
152
160
  try {
153
- const output = await action.run(toolCall.input);
161
+ const output = await (repeated && action.onRepeat
162
+ ? action.onRepeat(toolCall.input, context)
163
+ : action.run(toolCall.input, context));
154
164
  await chat.addToolOutput({
155
165
  tool: toolCall.toolName,
156
166
  toolCallId: toolCall.toolCallId,
@@ -174,6 +184,21 @@ function isRepeatInTurn(messages, action, toolCall) {
174
184
  getToolName(part) === action.name &&
175
185
  callKey(action, part.input) === key));
176
186
  }
187
+ /** The parts between the `step-start` before the call and the next one; none when the call is not in the last message. */
188
+ function stepOf(messages, toolCallId) {
189
+ const parts = messages.at(-1)?.parts ?? [];
190
+ const at = parts.findIndex((part) => isToolUIPart(part) && part.toolCallId === toolCallId);
191
+ if (at === -1) {
192
+ return [];
193
+ }
194
+ // The tsconfig lib predates `findLastIndex`.
195
+ const start = parts
196
+ .slice(0, at)
197
+ .map((part) => part.type)
198
+ .lastIndexOf('step-start');
199
+ const end = parts.findIndex((part, index) => index > at && part.type === 'step-start');
200
+ return parts.slice(start + 1, end === -1 ? undefined : end);
201
+ }
177
202
  /** The input with the model's nulls left out, so the same call matches however the model wrote it. */
178
203
  function callKey(action, input) {
179
204
  return JSON.stringify(normalizeModelNulls(action.inputSchema, input ?? {}));
@@ -1 +1 @@
1
- {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/client/actions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Client actions: tools the app carries out itself (open a screen, fill a\n * form), whose definitions go along with each request and whose results go\n * back as the turn's follow-up. A call reaches the app as the model wrote\n * it, without the server's input validation, so the app parses it here.\n */\n\nimport type { UIMessage } from 'ai'\nimport { getToolName, isToolUIPart } from 'ai'\nimport { z } from 'zod'\n\nimport { currentUserTurn } from './messages.js'\n\n// TYPES\n\n/** A tool that runs in the client. The server never interprets its name. */\nexport interface ClientAction<TName extends string = string> {\n\treadonly name: TName\n\treadonly description: string\n\treadonly inputSchema: z.ZodType\n\treadonly requiresApproval?: boolean\n\t/** Runs the action on the model's raw input. Throw to report a failure: the model reads it as an error it can act on. */\n\treadonly run: (input: unknown) => Promise<unknown>\n}\n\n/** A client action as a request carries it, for the server to offer the model. */\nexport interface ClientToolDef {\n\treadonly name: string\n\treadonly description: string\n\treadonly jsonSchema: unknown\n\treadonly requiresApproval?: boolean\n}\n\n/** A tool call the stream handed to the client. */\nexport interface ClientToolCall {\n\treadonly toolCallId: string\n\treadonly toolName: string\n\treadonly input: unknown\n}\n\n/** The part of the AI SDK `Chat` a client action needs. */\nexport type ClientActionChat = {\n\treadonly messages: UIMessage[]\n\treadonly addToolOutput: (\n\t\toptions:\n\t\t\t| { tool: string; toolCallId: string; output: unknown }\n\t\t\t| {\n\t\t\t\t\tstate: 'output-error'\n\t\t\t\t\ttool: string\n\t\t\t\t\ttoolCallId: string\n\t\t\t\t\terrorText: string\n\t\t\t }\n\t) => void | PromiseLike<void>\n}\n\ntype SchemaDef = {\n\treadonly type: string\n\treadonly innerType?: z.ZodType\n\treadonly element?: z.ZodType\n\treadonly valueType?: z.ZodType\n\treadonly in?: z.ZodType\n\treadonly options?: readonly z.ZodType[]\n\treadonly getter?: () => z.ZodType\n}\n\n// CONSTANTS\n\n/** What the model reads when an action throws something that is not an Error. */\nexport const CLIENT_ACTION_FAILED = 'The action failed.'\n\n/** What the model reads when the app cannot read the call's input. */\nexport const CLIENT_ACTION_INVALID_INPUT =\n\t'The app could not read this input. Call the action again with input that fits its schema.'\n\n/** What the model reads for a call it already made with the same input in this turn. */\nexport const CLIENT_ACTION_REPEATED =\n\t'Already done with these values in this turn. Do not call it again; tell the user what changed.'\n\n// FUNCTIONS\n\n/**\n * A client action whose handler gets its input parsed, with the model's\n * `null`s taken out where the schema means \"may be left out\". With\n * `resultSummary`, the output is `{ summary, data }`, for a result the data\n * alone would leave the model guessing about.\n */\nexport function defineClientAction<\n\tconst TName extends string,\n\tTSchema extends z.ZodType,\n\tTOutput\n>(action: {\n\treadonly name: TName\n\treadonly description: string\n\treadonly inputSchema: TSchema\n\treadonly requiresApproval?: boolean\n\treadonly resultSummary?: string\n\treadonly handler: (input: z.output<TSchema>) => Promise<TOutput>\n}): ClientAction<TName> {\n\treturn {\n\t\tname: action.name,\n\t\tdescription: action.description,\n\t\tinputSchema: action.inputSchema,\n\t\t...(action.requiresApproval === undefined\n\t\t\t? {}\n\t\t\t: { requiresApproval: action.requiresApproval }),\n\t\trun: async (input) => {\n\t\t\tconst parsed = action.inputSchema.safeParse(\n\t\t\t\tnormalizeModelNulls(action.inputSchema, input ?? {})\n\t\t\t)\n\n\t\t\tif (!parsed.success) {\n\t\t\t\tthrow new Error(CLIENT_ACTION_INVALID_INPUT)\n\t\t\t}\n\n\t\t\tconst data = await action.handler(parsed.data)\n\n\t\t\treturn action.resultSummary === undefined\n\t\t\t\t? data\n\t\t\t\t: { summary: action.resultSummary, data }\n\t\t}\n\t}\n}\n\n/**\n * Model arguments with `null` taken out wherever the schema means \"may be\n * left out\", at any depth. An OpenAI model behind Mastra's schema layer\n * fills every field it leaves alone with `null`; a `null` stays only where\n * the schema itself accepts it. Unions try each member and keep the first\n * reading that parses.\n */\nexport function normalizeModelNulls(\n\tschema: z.ZodType,\n\tvalue: unknown\n): unknown {\n\tconst def = schema._zod.def as unknown as SchemaDef\n\n\tswitch (def.type) {\n\t\tcase 'optional':\n\t\tcase 'nullable':\n\t\tcase 'default':\n\t\tcase 'prefault':\n\t\tcase 'readonly':\n\t\tcase 'catch':\n\t\t\treturn value == null || def.innerType === undefined\n\t\t\t\t? value\n\t\t\t\t: normalizeModelNulls(def.innerType, value)\n\t\tcase 'pipe':\n\t\t\treturn def.in === undefined ? value : normalizeModelNulls(def.in, value)\n\t\tcase 'lazy':\n\t\t\treturn def.getter === undefined\n\t\t\t\t? value\n\t\t\t\t: normalizeModelNulls(def.getter(), value)\n\t\tcase 'array': {\n\t\t\tconst element = def.element\n\n\t\t\treturn Array.isArray(value) && element !== undefined\n\t\t\t\t? value.map((item) => normalizeModelNulls(element, item))\n\t\t\t\t: value\n\t\t}\n\t\tcase 'record': {\n\t\t\tconst valueType = def.valueType\n\n\t\t\tif (!isPlainObject(value) || valueType === undefined) {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(value).flatMap(([key, item]) =>\n\t\t\t\t\titem === null && !valueType.safeParse(null).success\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [[key, normalizeModelNulls(valueType, item)]]\n\t\t\t\t)\n\t\t\t)\n\t\t}\n\t\tcase 'union': {\n\t\t\tfor (const option of def.options ?? []) {\n\t\t\t\tconst normalized = normalizeModelNulls(option, value)\n\n\t\t\t\tif (option.safeParse(normalized).success) {\n\t\t\t\t\treturn normalized\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn value\n\t\t}\n\t\tcase 'object': {\n\t\t\tif (!isPlainObject(value) || !(schema instanceof z.ZodObject)) {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\tconst shape = schema.shape as Record<string, z.ZodType>\n\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(value).flatMap(([key, item]) => {\n\t\t\t\t\tconst field = shape[key]\n\n\t\t\t\t\tif (field === undefined) {\n\t\t\t\t\t\treturn [[key, item]]\n\t\t\t\t\t}\n\n\t\t\t\t\tconst leaveOut =\n\t\t\t\t\t\titem === null &&\n\t\t\t\t\t\tfield.safeParse(undefined).success &&\n\t\t\t\t\t\t!field.safeParse(null).success\n\n\t\t\t\t\treturn leaveOut ? [] : [[key, normalizeModelNulls(field, item)]]\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\tdefault:\n\t\t\treturn value\n\t}\n}\n\n/** The definitions a request carries, for the server to offer the model as client tools. */\nexport function toClientToolDefs(\n\tactions: readonly ClientAction[]\n): ClientToolDef[] {\n\treturn actions.map((action) => ({\n\t\tname: action.name,\n\t\tdescription: action.description,\n\t\tjsonSchema: z.toJSONSchema(action.inputSchema, {\n\t\t\tio: 'input',\n\t\t\tunrepresentable: 'any'\n\t\t}),\n\t\t...(action.requiresApproval === undefined\n\t\t\t? {}\n\t\t\t: { requiresApproval: action.requiresApproval })\n\t}))\n}\n\n/**\n * Carries out the client action a tool call names and posts its result on the\n * chat; null when no action has that name (a server tool: its result arrives\n * on the stream). A call the user turn already made with the same input\n * (`null`s aside) is refused instead of carried out again.\n *\n * Never await it inside the chat's `onToolCall`: the AI SDK runs that callback\n * on the queue `addToolOutput` waits on, so awaiting deadlocks the stream.\n */\nexport function runClientAction(\n\tchat: ClientActionChat,\n\tactions: readonly ClientAction[],\n\ttoolCall: ClientToolCall\n): Promise<void> | null {\n\tconst action = actions.find(\n\t\t(candidate) => candidate.name === toolCall.toolName\n\t)\n\n\tif (action === undefined) {\n\t\treturn null\n\t}\n\n\tconst fail = (errorText: string) =>\n\t\tchat.addToolOutput({\n\t\t\tstate: 'output-error',\n\t\t\ttool: toolCall.toolName,\n\t\t\ttoolCallId: toolCall.toolCallId,\n\t\t\terrorText\n\t\t})\n\n\treturn (async () => {\n\t\tif (isRepeatInTurn(chat.messages, action, toolCall)) {\n\t\t\tawait fail(CLIENT_ACTION_REPEATED)\n\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst output = await action.run(toolCall.input)\n\n\t\t\tawait chat.addToolOutput({\n\t\t\t\ttool: toolCall.toolName,\n\t\t\t\ttoolCallId: toolCall.toolCallId,\n\t\t\t\toutput\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tawait fail(error instanceof Error ? error.message : CLIENT_ACTION_FAILED)\n\t\t}\n\t})()\n}\n\n/**\n * The same action with the same input already ran in this user turn. Running\n * it again changes nothing and only feeds a model that is circling.\n */\nfunction isRepeatInTurn(\n\tmessages: readonly UIMessage[],\n\taction: ClientAction,\n\ttoolCall: ClientToolCall\n): boolean {\n\tconst key = callKey(action, toolCall.input)\n\n\treturn currentUserTurn(messages).some((message) =>\n\t\tmessage.parts.some(\n\t\t\t(part) =>\n\t\t\t\tisToolUIPart(part) &&\n\t\t\t\tpart.toolCallId !== toolCall.toolCallId &&\n\t\t\t\tpart.state === 'output-available' &&\n\t\t\t\tgetToolName(part) === action.name &&\n\t\t\t\tcallKey(action, part.input) === key\n\t\t)\n\t)\n}\n\n/** The input with the model's nulls left out, so the same call matches however the model wrote it. */\nfunction callKey(action: ClientAction, input: unknown): string {\n\treturn JSON.stringify(normalizeModelNulls(action.inputSchema, input ?? {}))\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"]}
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/client/actions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Client actions: tools the app carries out itself (open a screen, fill a\n * form), whose definitions go along with each request and whose results go\n * back as the turn's follow-up. A call reaches the app as the model wrote\n * it, without the server's input validation, so the app parses it here.\n */\n\nimport type { UIMessage } from 'ai'\nimport { getToolName, isToolUIPart } from 'ai'\nimport { z } from 'zod'\n\nimport { currentUserTurn } from './messages.js'\n\n// TYPES\n\ntype MessagePart = UIMessage['parts'][number]\n\n/** Where the call sits, for an action with its own policy on repeats or shared steps. */\nexport interface ClientActionContext {\n\treadonly toolCallId: string\n\t/** The chat's messages when the call arrived; the call is in the last one. */\n\treadonly messages: readonly UIMessage[]\n\t/** The parts of the model step that made the call, the call and any server tool beside it included. */\n\treadonly step: readonly MessagePart[]\n}\n\n/** A tool that runs in the client. The server never interprets its name. */\nexport interface ClientAction<TName extends string = string> {\n\treadonly name: TName\n\treadonly description: string\n\treadonly inputSchema: z.ZodType\n\treadonly requiresApproval?: boolean\n\t/** Runs the action on the model's raw input. Throw to report a failure: the model reads it as an error it can act on. */\n\treadonly run: (\n\t\tinput: unknown,\n\t\tcontext: ClientActionContext\n\t) => Promise<unknown>\n\t/**\n\t * A call the user turn already made with the same input. Its result is\n\t * posted as the call's output; throw to post an error. Default: an error\n\t * with `CLIENT_ACTION_REPEATED`.\n\t */\n\treadonly onRepeat?: (\n\t\tinput: unknown,\n\t\tcontext: ClientActionContext\n\t) => Promise<unknown>\n}\n\n/** A client action as a request carries it, for the server to offer the model. */\nexport interface ClientToolDef {\n\treadonly name: string\n\treadonly description: string\n\treadonly jsonSchema: unknown\n\treadonly requiresApproval?: boolean\n}\n\n/** A tool call the stream handed to the client. */\nexport interface ClientToolCall {\n\treadonly toolCallId: string\n\treadonly toolName: string\n\treadonly input: unknown\n}\n\n/** The part of the AI SDK `Chat` a client action needs. */\nexport type ClientActionChat = {\n\treadonly messages: UIMessage[]\n\treadonly addToolOutput: (\n\t\toptions:\n\t\t\t| { tool: string; toolCallId: string; output: unknown }\n\t\t\t| {\n\t\t\t\t\tstate: 'output-error'\n\t\t\t\t\ttool: string\n\t\t\t\t\ttoolCallId: string\n\t\t\t\t\terrorText: string\n\t\t\t }\n\t) => void | PromiseLike<void>\n}\n\ntype SchemaDef = {\n\treadonly type: string\n\treadonly innerType?: z.ZodType\n\treadonly element?: z.ZodType\n\treadonly valueType?: z.ZodType\n\treadonly in?: z.ZodType\n\treadonly options?: readonly z.ZodType[]\n\treadonly getter?: () => z.ZodType\n}\n\n// CONSTANTS\n\n/** What the model reads when an action throws something that is not an Error. */\nexport const CLIENT_ACTION_FAILED = 'The action failed.'\n\n/** What the model reads when the app cannot read the call's input. */\nexport const CLIENT_ACTION_INVALID_INPUT =\n\t'The app could not read this input. Call the action again with input that fits its schema.'\n\n/** What the model reads for a call it already made with the same input in this turn. */\nexport const CLIENT_ACTION_REPEATED =\n\t'Already done with these values in this turn. Do not call it again; tell the user what changed.'\n\n// FUNCTIONS\n\n/**\n * A client action whose handler gets its input parsed, with the model's\n * `null`s taken out where the schema means \"may be left out\". With\n * `resultSummary`, the output is `{ summary, data }`, for a result the data\n * alone would leave the model guessing about.\n */\nexport function defineClientAction<\n\tconst TName extends string,\n\tTSchema extends z.ZodType,\n\tTOutput\n>(action: {\n\treadonly name: TName\n\treadonly description: string\n\treadonly inputSchema: TSchema\n\treadonly requiresApproval?: boolean\n\treadonly resultSummary?: string\n\treadonly handler: (\n\t\tinput: z.output<TSchema>,\n\t\tcontext: ClientActionContext\n\t) => Promise<TOutput>\n\treadonly onRepeat?: ClientAction['onRepeat']\n}): ClientAction<TName> {\n\treturn {\n\t\tname: action.name,\n\t\tdescription: action.description,\n\t\tinputSchema: action.inputSchema,\n\t\t...(action.requiresApproval === undefined\n\t\t\t? {}\n\t\t\t: { requiresApproval: action.requiresApproval }),\n\t\t...(action.onRepeat === undefined ? {} : { onRepeat: action.onRepeat }),\n\t\trun: async (input, context) => {\n\t\t\tconst parsed = action.inputSchema.safeParse(\n\t\t\t\tnormalizeModelNulls(action.inputSchema, input ?? {})\n\t\t\t)\n\n\t\t\tif (!parsed.success) {\n\t\t\t\tthrow new Error(CLIENT_ACTION_INVALID_INPUT)\n\t\t\t}\n\n\t\t\tconst data = await action.handler(parsed.data, context)\n\n\t\t\treturn action.resultSummary === undefined\n\t\t\t\t? data\n\t\t\t\t: { summary: action.resultSummary, data }\n\t\t}\n\t}\n}\n\n/**\n * Model arguments with `null` taken out wherever the schema means \"may be\n * left out\", at any depth. An OpenAI model behind Mastra's schema layer\n * fills every field it leaves alone with `null`; a `null` stays only where\n * the schema itself accepts it. Unions try each member and keep the first\n * reading that parses.\n */\nexport function normalizeModelNulls(\n\tschema: z.ZodType,\n\tvalue: unknown\n): unknown {\n\tconst def = schema._zod.def as unknown as SchemaDef\n\n\tswitch (def.type) {\n\t\tcase 'optional':\n\t\tcase 'nullable':\n\t\tcase 'default':\n\t\tcase 'prefault':\n\t\tcase 'readonly':\n\t\tcase 'catch':\n\t\t\treturn value == null || def.innerType === undefined\n\t\t\t\t? value\n\t\t\t\t: normalizeModelNulls(def.innerType, value)\n\t\tcase 'pipe':\n\t\t\treturn def.in === undefined ? value : normalizeModelNulls(def.in, value)\n\t\tcase 'lazy':\n\t\t\treturn def.getter === undefined\n\t\t\t\t? value\n\t\t\t\t: normalizeModelNulls(def.getter(), value)\n\t\tcase 'array': {\n\t\t\tconst element = def.element\n\n\t\t\treturn Array.isArray(value) && element !== undefined\n\t\t\t\t? value.map((item) => normalizeModelNulls(element, item))\n\t\t\t\t: value\n\t\t}\n\t\tcase 'record': {\n\t\t\tconst valueType = def.valueType\n\n\t\t\tif (!isPlainObject(value) || valueType === undefined) {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(value).flatMap(([key, item]) =>\n\t\t\t\t\titem === null && !valueType.safeParse(null).success\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [[key, normalizeModelNulls(valueType, item)]]\n\t\t\t\t)\n\t\t\t)\n\t\t}\n\t\tcase 'union': {\n\t\t\tfor (const option of def.options ?? []) {\n\t\t\t\tconst normalized = normalizeModelNulls(option, value)\n\n\t\t\t\tif (option.safeParse(normalized).success) {\n\t\t\t\t\treturn normalized\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn value\n\t\t}\n\t\tcase 'object': {\n\t\t\tif (!isPlainObject(value) || !(schema instanceof z.ZodObject)) {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\tconst shape = schema.shape as Record<string, z.ZodType>\n\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(value).flatMap(([key, item]) => {\n\t\t\t\t\tconst field = shape[key]\n\n\t\t\t\t\tif (field === undefined) {\n\t\t\t\t\t\treturn [[key, item]]\n\t\t\t\t\t}\n\n\t\t\t\t\tconst leaveOut =\n\t\t\t\t\t\titem === null &&\n\t\t\t\t\t\tfield.safeParse(undefined).success &&\n\t\t\t\t\t\t!field.safeParse(null).success\n\n\t\t\t\t\treturn leaveOut ? [] : [[key, normalizeModelNulls(field, item)]]\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\tdefault:\n\t\t\treturn value\n\t}\n}\n\n/** The definitions a request carries, for the server to offer the model as client tools. */\nexport function toClientToolDefs(\n\tactions: readonly ClientAction[]\n): ClientToolDef[] {\n\treturn actions.map((action) => ({\n\t\tname: action.name,\n\t\tdescription: action.description,\n\t\tjsonSchema: z.toJSONSchema(action.inputSchema, {\n\t\t\tio: 'input',\n\t\t\tunrepresentable: 'any'\n\t\t}),\n\t\t...(action.requiresApproval === undefined\n\t\t\t? {}\n\t\t\t: { requiresApproval: action.requiresApproval })\n\t}))\n}\n\n/**\n * Carries out the client action a tool call names and posts its result on the\n * chat; null when no action has that name (a server tool: its result arrives\n * on the stream). A call the user turn already made with the same input\n * (`null`s aside) is refused instead of carried out again, unless the action\n * has its own `onRepeat`.\n *\n * Never await it inside the chat's `onToolCall`: the AI SDK runs that callback\n * on the queue `addToolOutput` waits on, so awaiting deadlocks the stream.\n */\nexport function runClientAction(\n\tchat: ClientActionChat,\n\tactions: readonly ClientAction[],\n\ttoolCall: ClientToolCall\n): Promise<void> | null {\n\tconst action = actions.find(\n\t\t(candidate) => candidate.name === toolCall.toolName\n\t)\n\n\tif (action === undefined) {\n\t\treturn null\n\t}\n\n\tconst fail = (errorText: string) =>\n\t\tchat.addToolOutput({\n\t\t\tstate: 'output-error',\n\t\t\ttool: toolCall.toolName,\n\t\t\ttoolCallId: toolCall.toolCallId,\n\t\t\terrorText\n\t\t})\n\n\tconst context: ClientActionContext = {\n\t\ttoolCallId: toolCall.toolCallId,\n\t\tmessages: chat.messages,\n\t\tstep: stepOf(chat.messages, toolCall.toolCallId)\n\t}\n\n\treturn (async () => {\n\t\tconst repeated = isRepeatInTurn(chat.messages, action, toolCall)\n\n\t\tif (repeated && action.onRepeat === undefined) {\n\t\t\tawait fail(CLIENT_ACTION_REPEATED)\n\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst output = await (repeated && action.onRepeat\n\t\t\t\t? action.onRepeat(toolCall.input, context)\n\t\t\t\t: action.run(toolCall.input, context))\n\n\t\t\tawait chat.addToolOutput({\n\t\t\t\ttool: toolCall.toolName,\n\t\t\t\ttoolCallId: toolCall.toolCallId,\n\t\t\t\toutput\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tawait fail(error instanceof Error ? error.message : CLIENT_ACTION_FAILED)\n\t\t}\n\t})()\n}\n\n/**\n * The same action with the same input already ran in this user turn. Running\n * it again changes nothing and only feeds a model that is circling.\n */\nfunction isRepeatInTurn(\n\tmessages: readonly UIMessage[],\n\taction: ClientAction,\n\ttoolCall: ClientToolCall\n): boolean {\n\tconst key = callKey(action, toolCall.input)\n\n\treturn currentUserTurn(messages).some((message) =>\n\t\tmessage.parts.some(\n\t\t\t(part) =>\n\t\t\t\tisToolUIPart(part) &&\n\t\t\t\tpart.toolCallId !== toolCall.toolCallId &&\n\t\t\t\tpart.state === 'output-available' &&\n\t\t\t\tgetToolName(part) === action.name &&\n\t\t\t\tcallKey(action, part.input) === key\n\t\t)\n\t)\n}\n\n/** The parts between the `step-start` before the call and the next one; none when the call is not in the last message. */\nfunction stepOf(\n\tmessages: readonly UIMessage[],\n\ttoolCallId: string\n): MessagePart[] {\n\tconst parts = messages.at(-1)?.parts ?? []\n\tconst at = parts.findIndex(\n\t\t(part) => isToolUIPart(part) && part.toolCallId === toolCallId\n\t)\n\n\tif (at === -1) {\n\t\treturn []\n\t}\n\n\t// The tsconfig lib predates `findLastIndex`.\n\tconst start = parts\n\t\t.slice(0, at)\n\t\t.map((part) => part.type)\n\t\t.lastIndexOf('step-start')\n\tconst end = parts.findIndex(\n\t\t(part, index) => index > at && part.type === 'step-start'\n\t)\n\n\treturn parts.slice(start + 1, end === -1 ? undefined : end)\n}\n\n/** The input with the model's nulls left out, so the same call matches however the model wrote it. */\nfunction callKey(action: ClientAction, input: unknown): string {\n\treturn JSON.stringify(normalizeModelNulls(action.inputSchema, input ?? {}))\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"]}
@@ -61,13 +61,28 @@ export declare function toMessageBlocks(message: UIMessage): MessageBlock[];
61
61
  export declare function selectRequestMessages(messages: readonly UIMessage[], options?: RequestMessageOptions): UIMessage[];
62
62
  /** From the last user message to the end; the trailing message alone when there is no user message. */
63
63
  export declare function currentUserTurn(messages: readonly UIMessage[]): UIMessage[];
64
+ /** Which parts an answer resumes after: Mastra stores the continuation as a message of its own. */
65
+ export type ResumePointOptions = {
66
+ /** Default `isToolResultPart`: after any tool's result. */
67
+ readonly isResumePoint?: (part: MessagePart) => boolean;
68
+ };
69
+ /** A tool call with its output or error, the default resume point. */
70
+ export declare function isToolResultPart(part: MessagePart): boolean;
71
+ /**
72
+ * Joins an assistant message into the assistant message before it when that
73
+ * one ends at a resume point. Mastra stores the text a run wrote after
74
+ * resuming from a client tool's result as a new message, while the client
75
+ * streamed it into the message that holds the call; joining keeps that
76
+ * message and its id.
77
+ */
78
+ export declare function joinResumedAnswers<M extends UIMessage>(messages: readonly M[], options?: ResumePointOptions): M[];
64
79
  /**
65
80
  * The stored messages, oldest first, then the live ones. A message in both
66
81
  * shows once, as the live copy, which is the one still changing. An answer
67
- * resumed after a client action's result shows as one answer: Mastra stores
68
- * the continuation as its own message.
82
+ * resumed after a client action's result shows as one answer (see
83
+ * `joinResumedAnswers`).
69
84
  */
70
- export declare function mergeThreadMessages<M extends UIMessage>(stored: readonly M[], live: readonly M[]): M[];
85
+ export declare function mergeThreadMessages<M extends UIMessage>(stored: readonly M[], live: readonly M[], options?: ResumePointOptions): M[];
71
86
  /**
72
87
  * Server tool calls a request left at `input-available`: Mastra drops a
73
88
  * server result when a client action shares its step. The session answers
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/client/messages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/client/messages.ts"],"names":[],"mappings":"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"}
@@ -98,27 +98,27 @@ export function currentUserTurn(messages) {
98
98
  const trailing = messages.at(-1);
99
99
  return trailing === undefined ? [] : [trailing];
100
100
  }
101
+ /** A tool call with its output or error, the default resume point. */
102
+ export function isToolResultPart(part) {
103
+ return (isToolUIPart(part) &&
104
+ (part.state === 'output-available' || part.state === 'output-error'));
105
+ }
101
106
  /**
102
- * The stored messages, oldest first, then the live ones. A message in both
103
- * shows once, as the live copy, which is the one still changing. An answer
104
- * resumed after a client action's result shows as one answer: Mastra stores
105
- * the continuation as its own message.
107
+ * Joins an assistant message into the assistant message before it when that
108
+ * one ends at a resume point. Mastra stores the text a run wrote after
109
+ * resuming from a client tool's result as a new message, while the client
110
+ * streamed it into the message that holds the call; joining keeps that
111
+ * message and its id.
106
112
  */
107
- export function mergeThreadMessages(stored, live) {
108
- const liveIds = new Set(live.map((message) => message.id));
109
- const merged = [
110
- ...stored.filter((message) => !liveIds.has(message.id)),
111
- ...live
112
- ];
113
- return merged.reduce((joined, message) => {
113
+ export function joinResumedAnswers(messages, options = {}) {
114
+ const isResumePoint = options.isResumePoint ?? isToolResultPart;
115
+ return messages.reduce((joined, message) => {
114
116
  const previous = joined.at(-1);
115
117
  const lastPart = previous?.parts.at(-1);
116
118
  const resumesPrevious = message.role === 'assistant' &&
117
119
  previous?.role === 'assistant' &&
118
120
  lastPart !== undefined &&
119
- isToolUIPart(lastPart) &&
120
- (lastPart.state === 'output-available' ||
121
- lastPart.state === 'output-error');
121
+ isResumePoint(lastPart);
122
122
  if (previous !== undefined && resumesPrevious) {
123
123
  joined[joined.length - 1] = {
124
124
  ...previous,
@@ -131,6 +131,16 @@ export function mergeThreadMessages(stored, live) {
131
131
  return joined;
132
132
  }, []);
133
133
  }
134
+ /**
135
+ * The stored messages, oldest first, then the live ones. A message in both
136
+ * shows once, as the live copy, which is the one still changing. An answer
137
+ * resumed after a client action's result shows as one answer (see
138
+ * `joinResumedAnswers`).
139
+ */
140
+ export function mergeThreadMessages(stored, live, options = {}) {
141
+ const liveIds = new Set(live.map((message) => message.id));
142
+ return joinResumedAnswers([...stored.filter((message) => !liveIds.has(message.id)), ...live], options);
143
+ }
134
144
  /**
135
145
  * Server tool calls a request left at `input-available`: Mastra drops a
136
146
  * server result when a client action shares its step. The session answers
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/client/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Message helpers every client shares: which messages one request sends,\n * which parts reach the user, the stored thread merged with the live one,\n * tool calls a request left without a result, and the reply suggestions an\n * answer offers.\n */\n\nimport {\n\tgetToolName,\n\tisReasoningUIPart,\n\tisTextUIPart,\n\tisToolUIPart,\n\ttype UIMessage\n} from 'ai'\n\nimport { readSuspendedRun } from './pending.js'\n\n// TYPES\n\ntype MessagePart = UIMessage['parts'][number]\n\n/** A tool call in a message, from a typed tool or a dynamic one. */\nexport type ToolPart = Extract<MessagePart, { readonly toolCallId: string }>\n\n/** One block a message draws, in the order of its parts. */\nexport type MessageBlock =\n\t| {\n\t\t\treadonly kind: 'text'\n\t\t\treadonly key: string\n\t\t\treadonly text: string\n\t\t\t/** The AI SDK is still writing this text. */\n\t\t\treadonly streaming: boolean\n\t }\n\t/** Neighbouring tool parts: the cards of one step sit in one place between the texts. */\n\t| {\n\t\t\treadonly kind: 'tools'\n\t\t\treadonly key: string\n\t\t\treadonly parts: readonly ToolPart[]\n\t }\n\nexport interface RequestMessageOptions {\n\t/** Whether a tool part holds a result the client produced: a client action's result, or an error it posted. */\n\treadonly isClientResult?: (part: ToolPart) => boolean\n}\n\n// CONSTANTS\n\nconst OBSERVATIONAL_MEMORY_PART_PREFIX = 'data-om-'\n\n/** The transient data part a server streams after an answer: up to three replies the user can tap to send. */\nexport const REPLY_SUGGESTIONS_PART = 'data-reply-suggestions'\n\nexport const REPLY_SUGGESTIONS_MAX = 3\n\n/** The longest reply offered, in characters; a longer one is dropped. */\nexport const REPLY_SUGGESTION_MAX_LENGTH = 80\n\n/**\n * What the model reads for a server tool call a request left without a\n * result: Mastra drops a server result when a client action shares its step.\n */\nexport const STRANDED_TOOL_ERROR =\n\t'This tool call was interrupted because it ran in the same step as a client action. Call the tool again on its own to get the result.'\n\n// FUNCTIONS\n\n/** Whether a message part may reach the user: observational memory's `data-om-*` bookkeeping parts may not. */\nexport function isUserFacingPart(part: { readonly type: string }): boolean {\n\treturn !part.type.startsWith(OBSERVATIONAL_MEMORY_PART_PREFIX)\n}\n\n/** The message with only its user-facing parts. */\nexport function toUserFacingMessage<M extends UIMessage>(message: M): M {\n\treturn { ...message, parts: message.parts.filter(isUserFacingPart) }\n}\n\n/**\n * The blocks one message draws: its non-blank texts and its runs of tool\n * parts. Every other part, and every part observational memory adds, draws\n * nothing here; a client draws the data parts it knows itself.\n */\nexport function toMessageBlocks(message: UIMessage): MessageBlock[] {\n\tconst blocks: MessageBlock[] = []\n\n\tfor (const [index, part] of message.parts.entries()) {\n\t\tconst key = `${message.id}:${index}`\n\t\tconst previous = blocks.at(-1)\n\n\t\tif (!isUserFacingPart(part)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif (isTextUIPart(part) && part.text.trim() !== '') {\n\t\t\tblocks.push({\n\t\t\t\tkind: 'text',\n\t\t\t\tkey,\n\t\t\t\ttext: part.text,\n\t\t\t\tstreaming: part.state === 'streaming'\n\t\t\t})\n\t\t} else if (isToolUIPart(part)) {\n\t\t\tif (previous?.kind === 'tools') {\n\t\t\t\tblocks[blocks.length - 1] = {\n\t\t\t\t\t...previous,\n\t\t\t\t\tparts: [...previous.parts, part]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tblocks.push({ kind: 'tools', key, parts: [part] })\n\t\t\t}\n\t\t}\n\t}\n\n\treturn blocks\n}\n\n/**\n * The messages one request sends. Observational memory supplies the thread's\n * history, so a new user turn sends only the user's new message. When the\n * trailing assistant message carries results the client produced (client\n * action results, approval answers), the messages of the current user turn,\n * ending with that message. To resume a suspended run, the trailing assistant\n * message alone: Mastra resumes the run from its snapshot.\n */\nexport function selectRequestMessages(\n\tmessages: readonly UIMessage[],\n\toptions: RequestMessageOptions = {}\n): UIMessage[] {\n\tconst lastMessage = messages.at(-1)\n\n\tif (lastMessage === undefined) {\n\t\treturn []\n\t}\n\n\tswitch (lastMessage.role) {\n\t\tcase 'user':\n\t\t\treturn [lastMessage]\n\t\tcase 'assistant':\n\t\t\tif (\n\t\t\t\tlastMessage.parts.some((part) => readSuspendedRun(part) !== undefined)\n\t\t\t) {\n\t\t\t\treturn [lastMessage]\n\t\t\t}\n\n\t\t\treturn carriesClientResults(lastMessage, options)\n\t\t\t\t? currentUserTurn(messages)\n\t\t\t\t: lastUserMessage(messages)\n\t\tcase 'system':\n\t\t\treturn lastUserMessage(messages)\n\t}\n}\n\n/** From the last user message to the end; the trailing message alone when there is no user message. */\nexport function currentUserTurn(messages: readonly UIMessage[]): UIMessage[] {\n\tconst lastUserIndex = lastIndexOfUser(messages)\n\n\tif (lastUserIndex !== -1) {\n\t\treturn messages.slice(lastUserIndex)\n\t}\n\n\tconst trailing = messages.at(-1)\n\n\treturn trailing === undefined ? [] : [trailing]\n}\n\n/**\n * The stored messages, oldest first, then the live ones. A message in both\n * shows once, as the live copy, which is the one still changing. An answer\n * resumed after a client action's result shows as one answer: Mastra stores\n * the continuation as its own message.\n */\nexport function mergeThreadMessages<M extends UIMessage>(\n\tstored: readonly M[],\n\tlive: readonly M[]\n): M[] {\n\tconst liveIds = new Set(live.map((message) => message.id))\n\tconst merged = [\n\t\t...stored.filter((message) => !liveIds.has(message.id)),\n\t\t...live\n\t]\n\n\treturn merged.reduce<M[]>((joined, message) => {\n\t\tconst previous = joined.at(-1)\n\t\tconst lastPart = previous?.parts.at(-1)\n\t\tconst resumesPrevious =\n\t\t\tmessage.role === 'assistant' &&\n\t\t\tprevious?.role === 'assistant' &&\n\t\t\tlastPart !== undefined &&\n\t\t\tisToolUIPart(lastPart) &&\n\t\t\t(lastPart.state === 'output-available' ||\n\t\t\t\tlastPart.state === 'output-error')\n\n\t\tif (previous !== undefined && resumesPrevious) {\n\t\t\tjoined[joined.length - 1] = {\n\t\t\t\t...previous,\n\t\t\t\tparts: [...previous.parts, ...message.parts]\n\t\t\t}\n\t\t} else {\n\t\t\tjoined.push(message)\n\t\t}\n\n\t\treturn joined\n\t}, [])\n}\n\n/**\n * Server tool calls a request left at `input-available`: Mastra drops a\n * server result when a client action shares its step. The session answers\n * them with `STRANDED_TOOL_ERROR`, so the follow-up goes on instead of\n * hanging.\n */\nexport function findStrandedToolCalls(\n\tmessage: UIMessage | undefined,\n\tisClientAction: (toolName: string) => boolean\n): { readonly toolCallId: string; readonly toolName: string }[] {\n\tif (message?.role !== 'assistant') {\n\t\treturn []\n\t}\n\n\tconst suspended = new Set(\n\t\tmessage.parts.flatMap((part) => readSuspendedRun(part)?.toolCallId ?? [])\n\t)\n\n\treturn message.parts.flatMap((part) => {\n\t\tif (\n\t\t\t!isToolUIPart(part) ||\n\t\t\tpart.state !== 'input-available' ||\n\t\t\tpart.providerExecuted === true ||\n\t\t\tsuspended.has(part.toolCallId)\n\t\t) {\n\t\t\treturn []\n\t\t}\n\n\t\tconst toolName = getToolName(part)\n\n\t\treturn isClientAction(toolName)\n\t\t\t? []\n\t\t\t: [{ toolCallId: part.toolCallId, toolName }]\n\t})\n}\n\n/**\n * Whether the newest answer's latest step puts something on screen: answer\n * text, a running tool card, or shown reasoning. A step with only finished\n * tools or hidden reasoning is the model working out of sight, and a\n * \"Thinking…\" indicator has to cover that gap.\n */\nexport function isShowingProgress(\n\tmessages: readonly UIMessage[],\n\toptions: { readonly reasoningVisible?: boolean } = {}\n): boolean {\n\tconst newest = messages.at(-1)\n\n\tif (newest?.role !== 'assistant') {\n\t\treturn false\n\t}\n\n\tfor (let index = newest.parts.length - 1; index >= 0; index -= 1) {\n\t\tconst part = newest.parts[index]\n\n\t\tif (part === undefined || part.type === 'step-start') {\n\t\t\treturn false\n\t\t}\n\n\t\tif (isTextUIPart(part) && part.text.length > 0) {\n\t\t\treturn true\n\t\t}\n\n\t\tif (isToolUIPart(part)) {\n\t\t\treturn (\n\t\t\t\tpart.state === 'input-streaming' || part.state === 'input-available'\n\t\t\t)\n\t\t}\n\n\t\tif (\n\t\t\toptions.reasoningVisible &&\n\t\t\tisReasoningUIPart(part) &&\n\t\t\tpart.state === 'streaming'\n\t\t) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n/**\n * How many parts of an answer are on screen: answer text that is not blank,\n * and every tool part. Reasoning is not shown, and observational memory's\n * parts never are.\n */\nexport function countShownParts(\n\tmessage: UIMessage | undefined,\n\tisUserFacing: (part: MessagePart) => boolean = isUserFacingPart\n): number {\n\tif (message?.role !== 'assistant') {\n\t\treturn 0\n\t}\n\n\treturn message.parts.filter(\n\t\t(part) =>\n\t\t\tisUserFacing(part) &&\n\t\t\t((isTextUIPart(part) && part.text.trim() !== '') || isToolUIPart(part))\n\t).length\n}\n\n/**\n * The replies a reply-suggestions data part carries, trimmed, without empty,\n * too long or repeated ones, at most three; undefined when the data is not a\n * reply list.\n */\nexport function readReplySuggestions(\n\tdata: unknown\n): readonly string[] | undefined {\n\tconst suggestions = (data as { suggestions?: unknown } | null)?.suggestions\n\n\tif (!Array.isArray(suggestions)) {\n\t\treturn undefined\n\t}\n\n\treturn toReplySuggestions(\n\t\tsuggestions.map((suggestion) =>\n\t\t\ttypeof suggestion === 'string' ? suggestion : undefined\n\t\t)\n\t)\n}\n\n/** The replies worth offering from a model's list, in its order. */\nexport function toReplySuggestions(\n\tsuggestions: readonly (string | undefined)[]\n): readonly string[] {\n\tconst replies: string[] = []\n\n\tfor (const suggestion of suggestions) {\n\t\tconst reply = suggestion?.trim() ?? ''\n\t\tconst isOffered =\n\t\t\treply.length > 0 &&\n\t\t\treply.length <= REPLY_SUGGESTION_MAX_LENGTH &&\n\t\t\t!replies.some((offered) => offered.toLowerCase() === reply.toLowerCase())\n\n\t\tif (isOffered) {\n\t\t\treplies.push(reply)\n\t\t}\n\t}\n\n\treturn replies.slice(0, REPLY_SUGGESTIONS_MAX)\n}\n\nfunction carriesClientResults(\n\tmessage: UIMessage,\n\toptions: RequestMessageOptions\n): boolean {\n\treturn message.parts.some(\n\t\t(part) =>\n\t\t\tisToolUIPart(part) &&\n\t\t\t(part.state === 'approval-responded' ||\n\t\t\t\t((part.state === 'output-available' || part.state === 'output-error') &&\n\t\t\t\t\toptions.isClientResult?.(part) === true))\n\t)\n}\n\nfunction lastUserMessage(messages: readonly UIMessage[]): UIMessage[] {\n\tconst message = messages[lastIndexOfUser(messages)]\n\n\treturn message === undefined ? [] : [message]\n}\n\n/** `findLastIndex`, which the ES2022 build target lacks. */\nfunction lastIndexOfUser(messages: readonly UIMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tif (messages[index]?.role === 'user') {\n\t\t\treturn index\n\t\t}\n\t}\n\n\treturn -1\n}\n"]}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/client/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Message helpers every client shares: which messages one request sends,\n * which parts reach the user, the stored thread merged with the live one,\n * tool calls a request left without a result, and the reply suggestions an\n * answer offers.\n */\n\nimport {\n\tgetToolName,\n\tisReasoningUIPart,\n\tisTextUIPart,\n\tisToolUIPart,\n\ttype UIMessage\n} from 'ai'\n\nimport { readSuspendedRun } from './pending.js'\n\n// TYPES\n\ntype MessagePart = UIMessage['parts'][number]\n\n/** A tool call in a message, from a typed tool or a dynamic one. */\nexport type ToolPart = Extract<MessagePart, { readonly toolCallId: string }>\n\n/** One block a message draws, in the order of its parts. */\nexport type MessageBlock =\n\t| {\n\t\t\treadonly kind: 'text'\n\t\t\treadonly key: string\n\t\t\treadonly text: string\n\t\t\t/** The AI SDK is still writing this text. */\n\t\t\treadonly streaming: boolean\n\t }\n\t/** Neighbouring tool parts: the cards of one step sit in one place between the texts. */\n\t| {\n\t\t\treadonly kind: 'tools'\n\t\t\treadonly key: string\n\t\t\treadonly parts: readonly ToolPart[]\n\t }\n\nexport interface RequestMessageOptions {\n\t/** Whether a tool part holds a result the client produced: a client action's result, or an error it posted. */\n\treadonly isClientResult?: (part: ToolPart) => boolean\n}\n\n// CONSTANTS\n\nconst OBSERVATIONAL_MEMORY_PART_PREFIX = 'data-om-'\n\n/** The transient data part a server streams after an answer: up to three replies the user can tap to send. */\nexport const REPLY_SUGGESTIONS_PART = 'data-reply-suggestions'\n\nexport const REPLY_SUGGESTIONS_MAX = 3\n\n/** The longest reply offered, in characters; a longer one is dropped. */\nexport const REPLY_SUGGESTION_MAX_LENGTH = 80\n\n/**\n * What the model reads for a server tool call a request left without a\n * result: Mastra drops a server result when a client action shares its step.\n */\nexport const STRANDED_TOOL_ERROR =\n\t'This tool call was interrupted because it ran in the same step as a client action. Call the tool again on its own to get the result.'\n\n// FUNCTIONS\n\n/** Whether a message part may reach the user: observational memory's `data-om-*` bookkeeping parts may not. */\nexport function isUserFacingPart(part: { readonly type: string }): boolean {\n\treturn !part.type.startsWith(OBSERVATIONAL_MEMORY_PART_PREFIX)\n}\n\n/** The message with only its user-facing parts. */\nexport function toUserFacingMessage<M extends UIMessage>(message: M): M {\n\treturn { ...message, parts: message.parts.filter(isUserFacingPart) }\n}\n\n/**\n * The blocks one message draws: its non-blank texts and its runs of tool\n * parts. Every other part, and every part observational memory adds, draws\n * nothing here; a client draws the data parts it knows itself.\n */\nexport function toMessageBlocks(message: UIMessage): MessageBlock[] {\n\tconst blocks: MessageBlock[] = []\n\n\tfor (const [index, part] of message.parts.entries()) {\n\t\tconst key = `${message.id}:${index}`\n\t\tconst previous = blocks.at(-1)\n\n\t\tif (!isUserFacingPart(part)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif (isTextUIPart(part) && part.text.trim() !== '') {\n\t\t\tblocks.push({\n\t\t\t\tkind: 'text',\n\t\t\t\tkey,\n\t\t\t\ttext: part.text,\n\t\t\t\tstreaming: part.state === 'streaming'\n\t\t\t})\n\t\t} else if (isToolUIPart(part)) {\n\t\t\tif (previous?.kind === 'tools') {\n\t\t\t\tblocks[blocks.length - 1] = {\n\t\t\t\t\t...previous,\n\t\t\t\t\tparts: [...previous.parts, part]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tblocks.push({ kind: 'tools', key, parts: [part] })\n\t\t\t}\n\t\t}\n\t}\n\n\treturn blocks\n}\n\n/**\n * The messages one request sends. Observational memory supplies the thread's\n * history, so a new user turn sends only the user's new message. When the\n * trailing assistant message carries results the client produced (client\n * action results, approval answers), the messages of the current user turn,\n * ending with that message. To resume a suspended run, the trailing assistant\n * message alone: Mastra resumes the run from its snapshot.\n */\nexport function selectRequestMessages(\n\tmessages: readonly UIMessage[],\n\toptions: RequestMessageOptions = {}\n): UIMessage[] {\n\tconst lastMessage = messages.at(-1)\n\n\tif (lastMessage === undefined) {\n\t\treturn []\n\t}\n\n\tswitch (lastMessage.role) {\n\t\tcase 'user':\n\t\t\treturn [lastMessage]\n\t\tcase 'assistant':\n\t\t\tif (\n\t\t\t\tlastMessage.parts.some((part) => readSuspendedRun(part) !== undefined)\n\t\t\t) {\n\t\t\t\treturn [lastMessage]\n\t\t\t}\n\n\t\t\treturn carriesClientResults(lastMessage, options)\n\t\t\t\t? currentUserTurn(messages)\n\t\t\t\t: lastUserMessage(messages)\n\t\tcase 'system':\n\t\t\treturn lastUserMessage(messages)\n\t}\n}\n\n/** From the last user message to the end; the trailing message alone when there is no user message. */\nexport function currentUserTurn(messages: readonly UIMessage[]): UIMessage[] {\n\tconst lastUserIndex = lastIndexOfUser(messages)\n\n\tif (lastUserIndex !== -1) {\n\t\treturn messages.slice(lastUserIndex)\n\t}\n\n\tconst trailing = messages.at(-1)\n\n\treturn trailing === undefined ? [] : [trailing]\n}\n\n/** Which parts an answer resumes after: Mastra stores the continuation as a message of its own. */\nexport type ResumePointOptions = {\n\t/** Default `isToolResultPart`: after any tool's result. */\n\treadonly isResumePoint?: (part: MessagePart) => boolean\n}\n\n/** A tool call with its output or error, the default resume point. */\nexport function isToolResultPart(part: MessagePart): boolean {\n\treturn (\n\t\tisToolUIPart(part) &&\n\t\t(part.state === 'output-available' || part.state === 'output-error')\n\t)\n}\n\n/**\n * Joins an assistant message into the assistant message before it when that\n * one ends at a resume point. Mastra stores the text a run wrote after\n * resuming from a client tool's result as a new message, while the client\n * streamed it into the message that holds the call; joining keeps that\n * message and its id.\n */\nexport function joinResumedAnswers<M extends UIMessage>(\n\tmessages: readonly M[],\n\toptions: ResumePointOptions = {}\n): M[] {\n\tconst isResumePoint = options.isResumePoint ?? isToolResultPart\n\n\treturn messages.reduce<M[]>((joined, message) => {\n\t\tconst previous = joined.at(-1)\n\t\tconst lastPart = previous?.parts.at(-1)\n\t\tconst resumesPrevious =\n\t\t\tmessage.role === 'assistant' &&\n\t\t\tprevious?.role === 'assistant' &&\n\t\t\tlastPart !== undefined &&\n\t\t\tisResumePoint(lastPart)\n\n\t\tif (previous !== undefined && resumesPrevious) {\n\t\t\tjoined[joined.length - 1] = {\n\t\t\t\t...previous,\n\t\t\t\tparts: [...previous.parts, ...message.parts]\n\t\t\t}\n\t\t} else {\n\t\t\tjoined.push(message)\n\t\t}\n\n\t\treturn joined\n\t}, [])\n}\n\n/**\n * The stored messages, oldest first, then the live ones. A message in both\n * shows once, as the live copy, which is the one still changing. An answer\n * resumed after a client action's result shows as one answer (see\n * `joinResumedAnswers`).\n */\nexport function mergeThreadMessages<M extends UIMessage>(\n\tstored: readonly M[],\n\tlive: readonly M[],\n\toptions: ResumePointOptions = {}\n): M[] {\n\tconst liveIds = new Set(live.map((message) => message.id))\n\n\treturn joinResumedAnswers(\n\t\t[...stored.filter((message) => !liveIds.has(message.id)), ...live],\n\t\toptions\n\t)\n}\n\n/**\n * Server tool calls a request left at `input-available`: Mastra drops a\n * server result when a client action shares its step. The session answers\n * them with `STRANDED_TOOL_ERROR`, so the follow-up goes on instead of\n * hanging.\n */\nexport function findStrandedToolCalls(\n\tmessage: UIMessage | undefined,\n\tisClientAction: (toolName: string) => boolean\n): { readonly toolCallId: string; readonly toolName: string }[] {\n\tif (message?.role !== 'assistant') {\n\t\treturn []\n\t}\n\n\tconst suspended = new Set(\n\t\tmessage.parts.flatMap((part) => readSuspendedRun(part)?.toolCallId ?? [])\n\t)\n\n\treturn message.parts.flatMap((part) => {\n\t\tif (\n\t\t\t!isToolUIPart(part) ||\n\t\t\tpart.state !== 'input-available' ||\n\t\t\tpart.providerExecuted === true ||\n\t\t\tsuspended.has(part.toolCallId)\n\t\t) {\n\t\t\treturn []\n\t\t}\n\n\t\tconst toolName = getToolName(part)\n\n\t\treturn isClientAction(toolName)\n\t\t\t? []\n\t\t\t: [{ toolCallId: part.toolCallId, toolName }]\n\t})\n}\n\n/**\n * Whether the newest answer's latest step puts something on screen: answer\n * text, a running tool card, or shown reasoning. A step with only finished\n * tools or hidden reasoning is the model working out of sight, and a\n * \"Thinking…\" indicator has to cover that gap.\n */\nexport function isShowingProgress(\n\tmessages: readonly UIMessage[],\n\toptions: { readonly reasoningVisible?: boolean } = {}\n): boolean {\n\tconst newest = messages.at(-1)\n\n\tif (newest?.role !== 'assistant') {\n\t\treturn false\n\t}\n\n\tfor (let index = newest.parts.length - 1; index >= 0; index -= 1) {\n\t\tconst part = newest.parts[index]\n\n\t\tif (part === undefined || part.type === 'step-start') {\n\t\t\treturn false\n\t\t}\n\n\t\tif (isTextUIPart(part) && part.text.length > 0) {\n\t\t\treturn true\n\t\t}\n\n\t\tif (isToolUIPart(part)) {\n\t\t\treturn (\n\t\t\t\tpart.state === 'input-streaming' || part.state === 'input-available'\n\t\t\t)\n\t\t}\n\n\t\tif (\n\t\t\toptions.reasoningVisible &&\n\t\t\tisReasoningUIPart(part) &&\n\t\t\tpart.state === 'streaming'\n\t\t) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n/**\n * How many parts of an answer are on screen: answer text that is not blank,\n * and every tool part. Reasoning is not shown, and observational memory's\n * parts never are.\n */\nexport function countShownParts(\n\tmessage: UIMessage | undefined,\n\tisUserFacing: (part: MessagePart) => boolean = isUserFacingPart\n): number {\n\tif (message?.role !== 'assistant') {\n\t\treturn 0\n\t}\n\n\treturn message.parts.filter(\n\t\t(part) =>\n\t\t\tisUserFacing(part) &&\n\t\t\t((isTextUIPart(part) && part.text.trim() !== '') || isToolUIPart(part))\n\t).length\n}\n\n/**\n * The replies a reply-suggestions data part carries, trimmed, without empty,\n * too long or repeated ones, at most three; undefined when the data is not a\n * reply list.\n */\nexport function readReplySuggestions(\n\tdata: unknown\n): readonly string[] | undefined {\n\tconst suggestions = (data as { suggestions?: unknown } | null)?.suggestions\n\n\tif (!Array.isArray(suggestions)) {\n\t\treturn undefined\n\t}\n\n\treturn toReplySuggestions(\n\t\tsuggestions.map((suggestion) =>\n\t\t\ttypeof suggestion === 'string' ? suggestion : undefined\n\t\t)\n\t)\n}\n\n/** The replies worth offering from a model's list, in its order. */\nexport function toReplySuggestions(\n\tsuggestions: readonly (string | undefined)[]\n): readonly string[] {\n\tconst replies: string[] = []\n\n\tfor (const suggestion of suggestions) {\n\t\tconst reply = suggestion?.trim() ?? ''\n\t\tconst isOffered =\n\t\t\treply.length > 0 &&\n\t\t\treply.length <= REPLY_SUGGESTION_MAX_LENGTH &&\n\t\t\t!replies.some((offered) => offered.toLowerCase() === reply.toLowerCase())\n\n\t\tif (isOffered) {\n\t\t\treplies.push(reply)\n\t\t}\n\t}\n\n\treturn replies.slice(0, REPLY_SUGGESTIONS_MAX)\n}\n\nfunction carriesClientResults(\n\tmessage: UIMessage,\n\toptions: RequestMessageOptions\n): boolean {\n\treturn message.parts.some(\n\t\t(part) =>\n\t\t\tisToolUIPart(part) &&\n\t\t\t(part.state === 'approval-responded' ||\n\t\t\t\t((part.state === 'output-available' || part.state === 'output-error') &&\n\t\t\t\t\toptions.isClientResult?.(part) === true))\n\t)\n}\n\nfunction lastUserMessage(messages: readonly UIMessage[]): UIMessage[] {\n\tconst message = messages[lastIndexOfUser(messages)]\n\n\treturn message === undefined ? [] : [message]\n}\n\n/** `findLastIndex`, which the ES2022 build target lacks. */\nfunction lastIndexOfUser(messages: readonly UIMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tif (messages[index]?.role === 'user') {\n\t\t\treturn index\n\t\t}\n\t}\n\n\treturn -1\n}\n"]}
@@ -54,4 +54,6 @@ export declare function splitWordTokens(text: string): readonly WordToken[];
54
54
  export declare function createWordFadeTracker(now: () => number): WordFadeTracker;
55
55
  /** Whether a chat link may open: web, mail and phone links, and relative ones. */
56
56
  export declare function isSafeUrl(url: string): boolean;
57
+ /** Whether a chat link is a web link (`http:` or `https:`): the stricter choice where mail, phone and relative links should not open. */
58
+ export declare function isWebUrl(url: string): boolean;
57
59
  //# sourceMappingURL=reveal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reveal.d.ts","sourceRoot":"","sources":["../../src/client/reveal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,6DAA6D;AAC7D,eAAO,MAAM,cAAc,KAAK,CAAA;AAEhC;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC,uDAAuD;AACvD,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAWlC,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,eAAe;IAC/B,iGAAiG;IACjG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;KAC9C,CAAA;CACD;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAU1E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED,4GAA4G;AAC5G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAMlE;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAKlE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CA8BxE;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C"}
1
+ {"version":3,"file":"reveal.d.ts","sourceRoot":"","sources":["../../src/client/reveal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,6DAA6D;AAC7D,eAAO,MAAM,cAAc,KAAK,CAAA;AAEhC;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC,uDAAuD;AACvD,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAYlC,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,eAAe;IAC/B,iGAAiG;IACjG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG;QAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;KAC9C,CAAA;CACD;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAU1E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED,4GAA4G;AAC5G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAMlE;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAKlE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CA8BxE;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,yIAAyI;AACzI,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE7C"}
@@ -23,6 +23,7 @@ const PARAGRAPH_BREAK = /\n{2,}(?=\S)/;
23
23
  const CODE_FENCE = '```';
24
24
  /** Schemes and shapes a chat link may use. Anything else loses its href. */
25
25
  const SAFE_URL = /^(?:(?:https?|mailto|tel):|[#/]|\.{1,2}\/)/i;
26
+ const WEB_URL = /^https?:\/\//i;
26
27
  // FUNCTIONS
27
28
  /**
28
29
  * How much of the text the next reveal step shows: whole words only, one more
@@ -90,6 +91,10 @@ export function createWordFadeTracker(now) {
90
91
  export function isSafeUrl(url) {
91
92
  return SAFE_URL.test(url.trim());
92
93
  }
94
+ /** Whether a chat link is a web link (`http:` or `https:`): the stricter choice where mail, phone and relative links should not open. */
95
+ export function isWebUrl(url) {
96
+ return WEB_URL.test(url.trim());
97
+ }
93
98
  /** The end of every whole word after `from`; a word is whole once whitespace follows it. */
94
99
  function wordEndsAfter(text, from) {
95
100
  const wordEnd = /\S+(?=\s)/g;
@@ -1 +1 @@
1
- {"version":3,"file":"reveal.js","sourceRoot":"","sources":["../../src/client/reveal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a streaming answer's text is revealed and prepared for markdown: the\n * steady word-by-word pace, which revealed words are still fading in,\n * trimming markdown that is only half written, the split into paragraphs so\n * only the last one parses again as the text grows, and which links may open.\n * Pure; each binding runs the timer and draws the fade its own way.\n */\n\n// CONSTANTS\n\n/** Time between two reveal steps while an answer streams. */\nexport const REVEAL_STEP_MS = 40\n\n/**\n * Reveal steps a backlog of words is spread over: one word a step at the\n * steady pace, more when the stream runs ahead, so the reveal never lags far\n * behind the answer.\n */\nexport const REVEAL_CATCH_UP_STEPS = 8\n\n/** How long a newly revealed word takes to fade in. */\nexport const WORD_FADE_MS = 420\n\n/** At most this many words fade at once, so a large chunk landing together stays cheap. */\nexport const MAX_FADING_WORDS = 24\n\n/** A blank line before text that starts at the margin: an indented line after a blank line continues a list item. */\nconst PARAGRAPH_BREAK = /\\n{2,}(?=\\S)/\nconst CODE_FENCE = '```'\n\n/** Schemes and shapes a chat link may use. Anything else loses its href. */\nconst SAFE_URL = /^(?:(?:https?|mailto|tel):|[#/]|\\.{1,2}\\/)/i\n\n// TYPES\n\n/** A run of text that is either one word with the whitespace before it, or trailing whitespace. */\nexport interface WordToken {\n\treadonly text: string\n\treadonly word: boolean\n}\n\n/** Remembers when each word of one text block first appeared, so a re-render resumes its fade. */\nexport interface WordFadeTracker {\n\t/** Opens a render over `totalWords` words: the first fading one, and how long each has shown. */\n\twindow(totalWords: number): {\n\t\treadonly from: number\n\t\treadonly elapsedFor: (index: number) => number\n\t}\n}\n\n// FUNCTIONS\n\n/**\n * How much of the text the next reveal step shows: whole words only, one more\n * at the steady pace and a share of the backlog when the stream runs ahead. A\n * word still being written waits until it is whole.\n */\nexport function nextRevealLength(text: string, shownLength: number): number {\n\tconst wordEnds = wordEndsAfter(text, shownLength)\n\n\tif (wordEnds.length === 0) {\n\t\treturn shownLength\n\t}\n\n\tconst step = Math.ceil(wordEnds.length / REVEAL_CATCH_UP_STEPS)\n\n\treturn wordEnds[step - 1] ?? text.length\n}\n\n/**\n * Makes a streaming prefix render as its finished text will: a half-written\n * link shows its label, open bold, italics and code are closed, and a list\n * marker without its text waits for its first word. Only while the text\n * streams: finished text renders as written.\n */\nexport function trimIncompleteMarkdown(text: string): string {\n\tconst withoutBareMarker = text.replace(/\\n[ \\t]*(?:[-*+]|\\d+[.)])[ \\t]*$/, '')\n\n\treturn closeOpenMarkers(trimIncompleteLink(withoutBareMarker))\n}\n\n/** One string per paragraph. A fenced code block may hold blank lines, so text with a fence stays whole. */\nexport function markdownParagraphs(text: string): readonly string[] {\n\tif (text.includes(CODE_FENCE)) {\n\t\treturn [text]\n\t}\n\n\treturn text.split(PARAGRAPH_BREAK).filter((paragraph) => paragraph !== '')\n}\n\n/** Splits text into tokens that join back to it, each word carrying the whitespace before it. */\nexport function splitWordTokens(text: string): readonly WordToken[] {\n\treturn (text.match(/\\s*\\S+|\\s+$/g) ?? []).map((token) => ({\n\t\ttext: token,\n\t\tword: /\\S/.test(token)\n\t}))\n}\n\n/**\n * Stamps each word the first time a render counts it. Fewer words than before\n * means the text was replaced rather than grown, so all of it counts as settled.\n */\nexport function createWordFadeTracker(now: () => number): WordFadeTracker {\n\tlet firstSeenAt: number[] = []\n\n\treturn {\n\t\twindow(totalWords) {\n\t\t\tconst at = now()\n\n\t\t\tif (totalWords < firstSeenAt.length) {\n\t\t\t\tfirstSeenAt = Array.from(\n\t\t\t\t\t{ length: totalWords },\n\t\t\t\t\t() => at - WORD_FADE_MS\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tfor (let index = firstSeenAt.length; index < totalWords; index += 1) {\n\t\t\t\tfirstSeenAt.push(at)\n\t\t\t}\n\n\t\t\tconst elapsedFor = (index: number) =>\n\t\t\t\tMath.max(0, at - (firstSeenAt[index] ?? at))\n\t\t\t// Words are stamped in order, so the settled ones are a prefix.\n\t\t\tlet from = Math.max(0, totalWords - MAX_FADING_WORDS)\n\n\t\t\twhile (from < totalWords && elapsedFor(from) >= WORD_FADE_MS) {\n\t\t\t\tfrom += 1\n\t\t\t}\n\n\t\t\treturn { from, elapsedFor }\n\t\t}\n\t}\n}\n\n/** Whether a chat link may open: web, mail and phone links, and relative ones. */\nexport function isSafeUrl(url: string): boolean {\n\treturn SAFE_URL.test(url.trim())\n}\n\n/** The end of every whole word after `from`; a word is whole once whitespace follows it. */\nfunction wordEndsAfter(text: string, from: number): number[] {\n\tconst wordEnd = /\\S+(?=\\s)/g\n\twordEnd.lastIndex = from\n\n\treturn Array.from(\n\t\ttext.matchAll(wordEnd),\n\t\t(match) => match.index + match[0].length\n\t)\n}\n\nfunction trimIncompleteLink(text: string): string {\n\tconst withUrl = /!?\\[([^\\]]*)\\]\\([^)]*$/.exec(text)\n\n\tif (withUrl) {\n\t\treturn text.slice(0, withUrl.index) + withUrl[1]\n\t}\n\n\tconst withLabel = /!?\\[([^\\]]*)$/.exec(text)\n\n\tif (withLabel) {\n\t\treturn text.slice(0, withLabel.index) + withLabel[1]\n\t}\n\n\treturn text\n}\n\nfunction closeOpenMarkers(text: string): string {\n\tconst fences = countOccurrences(text, CODE_FENCE)\n\n\tif (fences % 2 === 1) {\n\t\treturn `${text}\\n${CODE_FENCE}`\n\t}\n\n\tconst outsideFences = text.replaceAll(CODE_FENCE, '')\n\tconst inlineCode = countOccurrences(outsideFences, '`') % 2 === 1 ? '`' : ''\n\tconst outsideCode = outsideFences\n\t\t.replace(/`[^`]*`?/g, '')\n\t\t.replace(/^[ \\t]*[*+-][ \\t]+/gm, '')\n\tconst bold = countOccurrences(outsideCode, '**') % 2 === 1 ? '**' : ''\n\tconst italic =\n\t\tcountOccurrences(outsideCode.replaceAll('**', ''), '*') % 2 === 1 ? '*' : ''\n\n\treturn `${text}${inlineCode}${italic}${bold}`\n}\n\nfunction countOccurrences(text: string, marker: string): number {\n\treturn text.split(marker).length - 1\n}\n"]}
1
+ {"version":3,"file":"reveal.js","sourceRoot":"","sources":["../../src/client/reveal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a streaming answer's text is revealed and prepared for markdown: the\n * steady word-by-word pace, which revealed words are still fading in,\n * trimming markdown that is only half written, the split into paragraphs so\n * only the last one parses again as the text grows, and which links may open.\n * Pure; each binding runs the timer and draws the fade its own way.\n */\n\n// CONSTANTS\n\n/** Time between two reveal steps while an answer streams. */\nexport const REVEAL_STEP_MS = 40\n\n/**\n * Reveal steps a backlog of words is spread over: one word a step at the\n * steady pace, more when the stream runs ahead, so the reveal never lags far\n * behind the answer.\n */\nexport const REVEAL_CATCH_UP_STEPS = 8\n\n/** How long a newly revealed word takes to fade in. */\nexport const WORD_FADE_MS = 420\n\n/** At most this many words fade at once, so a large chunk landing together stays cheap. */\nexport const MAX_FADING_WORDS = 24\n\n/** A blank line before text that starts at the margin: an indented line after a blank line continues a list item. */\nconst PARAGRAPH_BREAK = /\\n{2,}(?=\\S)/\nconst CODE_FENCE = '```'\n\n/** Schemes and shapes a chat link may use. Anything else loses its href. */\nconst SAFE_URL = /^(?:(?:https?|mailto|tel):|[#/]|\\.{1,2}\\/)/i\nconst WEB_URL = /^https?:\\/\\//i\n\n// TYPES\n\n/** A run of text that is either one word with the whitespace before it, or trailing whitespace. */\nexport interface WordToken {\n\treadonly text: string\n\treadonly word: boolean\n}\n\n/** Remembers when each word of one text block first appeared, so a re-render resumes its fade. */\nexport interface WordFadeTracker {\n\t/** Opens a render over `totalWords` words: the first fading one, and how long each has shown. */\n\twindow(totalWords: number): {\n\t\treadonly from: number\n\t\treadonly elapsedFor: (index: number) => number\n\t}\n}\n\n// FUNCTIONS\n\n/**\n * How much of the text the next reveal step shows: whole words only, one more\n * at the steady pace and a share of the backlog when the stream runs ahead. A\n * word still being written waits until it is whole.\n */\nexport function nextRevealLength(text: string, shownLength: number): number {\n\tconst wordEnds = wordEndsAfter(text, shownLength)\n\n\tif (wordEnds.length === 0) {\n\t\treturn shownLength\n\t}\n\n\tconst step = Math.ceil(wordEnds.length / REVEAL_CATCH_UP_STEPS)\n\n\treturn wordEnds[step - 1] ?? text.length\n}\n\n/**\n * Makes a streaming prefix render as its finished text will: a half-written\n * link shows its label, open bold, italics and code are closed, and a list\n * marker without its text waits for its first word. Only while the text\n * streams: finished text renders as written.\n */\nexport function trimIncompleteMarkdown(text: string): string {\n\tconst withoutBareMarker = text.replace(/\\n[ \\t]*(?:[-*+]|\\d+[.)])[ \\t]*$/, '')\n\n\treturn closeOpenMarkers(trimIncompleteLink(withoutBareMarker))\n}\n\n/** One string per paragraph. A fenced code block may hold blank lines, so text with a fence stays whole. */\nexport function markdownParagraphs(text: string): readonly string[] {\n\tif (text.includes(CODE_FENCE)) {\n\t\treturn [text]\n\t}\n\n\treturn text.split(PARAGRAPH_BREAK).filter((paragraph) => paragraph !== '')\n}\n\n/** Splits text into tokens that join back to it, each word carrying the whitespace before it. */\nexport function splitWordTokens(text: string): readonly WordToken[] {\n\treturn (text.match(/\\s*\\S+|\\s+$/g) ?? []).map((token) => ({\n\t\ttext: token,\n\t\tword: /\\S/.test(token)\n\t}))\n}\n\n/**\n * Stamps each word the first time a render counts it. Fewer words than before\n * means the text was replaced rather than grown, so all of it counts as settled.\n */\nexport function createWordFadeTracker(now: () => number): WordFadeTracker {\n\tlet firstSeenAt: number[] = []\n\n\treturn {\n\t\twindow(totalWords) {\n\t\t\tconst at = now()\n\n\t\t\tif (totalWords < firstSeenAt.length) {\n\t\t\t\tfirstSeenAt = Array.from(\n\t\t\t\t\t{ length: totalWords },\n\t\t\t\t\t() => at - WORD_FADE_MS\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tfor (let index = firstSeenAt.length; index < totalWords; index += 1) {\n\t\t\t\tfirstSeenAt.push(at)\n\t\t\t}\n\n\t\t\tconst elapsedFor = (index: number) =>\n\t\t\t\tMath.max(0, at - (firstSeenAt[index] ?? at))\n\t\t\t// Words are stamped in order, so the settled ones are a prefix.\n\t\t\tlet from = Math.max(0, totalWords - MAX_FADING_WORDS)\n\n\t\t\twhile (from < totalWords && elapsedFor(from) >= WORD_FADE_MS) {\n\t\t\t\tfrom += 1\n\t\t\t}\n\n\t\t\treturn { from, elapsedFor }\n\t\t}\n\t}\n}\n\n/** Whether a chat link may open: web, mail and phone links, and relative ones. */\nexport function isSafeUrl(url: string): boolean {\n\treturn SAFE_URL.test(url.trim())\n}\n\n/** Whether a chat link is a web link (`http:` or `https:`): the stricter choice where mail, phone and relative links should not open. */\nexport function isWebUrl(url: string): boolean {\n\treturn WEB_URL.test(url.trim())\n}\n\n/** The end of every whole word after `from`; a word is whole once whitespace follows it. */\nfunction wordEndsAfter(text: string, from: number): number[] {\n\tconst wordEnd = /\\S+(?=\\s)/g\n\twordEnd.lastIndex = from\n\n\treturn Array.from(\n\t\ttext.matchAll(wordEnd),\n\t\t(match) => match.index + match[0].length\n\t)\n}\n\nfunction trimIncompleteLink(text: string): string {\n\tconst withUrl = /!?\\[([^\\]]*)\\]\\([^)]*$/.exec(text)\n\n\tif (withUrl) {\n\t\treturn text.slice(0, withUrl.index) + withUrl[1]\n\t}\n\n\tconst withLabel = /!?\\[([^\\]]*)$/.exec(text)\n\n\tif (withLabel) {\n\t\treturn text.slice(0, withLabel.index) + withLabel[1]\n\t}\n\n\treturn text\n}\n\nfunction closeOpenMarkers(text: string): string {\n\tconst fences = countOccurrences(text, CODE_FENCE)\n\n\tif (fences % 2 === 1) {\n\t\treturn `${text}\\n${CODE_FENCE}`\n\t}\n\n\tconst outsideFences = text.replaceAll(CODE_FENCE, '')\n\tconst inlineCode = countOccurrences(outsideFences, '`') % 2 === 1 ? '`' : ''\n\tconst outsideCode = outsideFences\n\t\t.replace(/`[^`]*`?/g, '')\n\t\t.replace(/^[ \\t]*[*+-][ \\t]+/gm, '')\n\tconst bold = countOccurrences(outsideCode, '**') % 2 === 1 ? '**' : ''\n\tconst italic =\n\t\tcountOccurrences(outsideCode.replaceAll('**', ''), '*') % 2 === 1 ? '*' : ''\n\n\treturn `${text}${inlineCode}${italic}${bold}`\n}\n\nfunction countOccurrences(text: string, marker: string): number {\n\treturn text.split(marker).length - 1\n}\n"]}