juneau 0.2.5 → 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.
package/README.md CHANGED
@@ -182,6 +182,64 @@ export const myAdapter: AiBackendAdapter = {
182
182
 
183
183
  ---
184
184
 
185
+ ### Backend utilities — `juneau/server`
186
+
187
+ If your backend uses ai-sdk, Juneau ships server-side helpers that eliminate the boilerplate of history mapping, activity streaming, and tool failure recovery. Import from the `/server` subpath — zod stays out of the browser bundle.
188
+
189
+ ```ts
190
+ import { toSdkMessages, createSkillSet, withToolRecovery } from 'juneau/server';
191
+ ```
192
+
193
+ **Full integration in ~15 lines:**
194
+
195
+ ```ts
196
+ import { toSdkMessages, createSkillSet, withToolRecovery } from 'juneau/server';
197
+ import { streamText } from 'ai';
198
+ import { z } from 'zod';
199
+
200
+ const skillSet = createSkillSet({
201
+ search: {
202
+ description: 'Search for records.',
203
+ input: z.object({ query: z.string() }),
204
+ labels: {
205
+ running: { en: 'Searching…', cs: 'Vyhledávám…' },
206
+ done: { en: 'Results found', cs: 'Nalezeno' },
207
+ },
208
+ execute: async ({ query }) => db.search(query),
209
+ },
210
+ }, { language: context.language });
211
+
212
+ for await (const chunk of withToolRecovery({
213
+ phase1: () => streamText({ model, system, messages: toSdkMessages(input.messages), tools: skillSet.tools, maxSteps: 1 }),
214
+ phase2: (ctx) => streamText({ model, system, messages: [...toSdkMessages(input.messages), { role: 'assistant', content: ctx }] }),
215
+ skillSet,
216
+ })) {
217
+ res.write(chunk);
218
+ }
219
+ ```
220
+
221
+ #### `toSdkMessages(messages)`
222
+
223
+ Converts `AiMessage[]` to `CoreMessage[]` for ai-sdk. Extracts text from parts, fixes conversation alternation (no two consecutive user turns), filters empty turns.
224
+
225
+ #### `createSkillSet(skills, options?)`
226
+
227
+ Wraps skill definitions into ai-sdk `tools` with a built-in activity buffer. Each tool call automatically emits `running` / `done` / `failed` Juneau wire SSE strings — no manual activity handling needed.
228
+
229
+ `SkillSet` members: `tools`, `drainActivities()`, `hadFailure`, `failureContext`.
230
+
231
+ `SkillSetOptions`: `language?` — selects label variant (`'en'` default).
232
+
233
+ #### `streamToWire(fullStream, skillSet?)`
234
+
235
+ Converts ai-sdk `fullStream` to Juneau wire SSE strings. Handles ai-sdk v6/v7 chunk rename, drains activity buffer at the right moment, emits `done` at the end.
236
+
237
+ #### `withToolRecovery(options)`
238
+
239
+ Two-phase pattern for Gemini-style tool failures — when a tool fails and the model loops on retrying instead of writing text, phase 2 calls the model without tools and injects the failure context to force a text response.
240
+
241
+ ---
242
+
185
243
  ### Juneau wire protocol — for Juneau-compatible backends
186
244
 
187
245
  If your backend is built specifically for Juneau (e.g. Tappeer), stream newline-delimited JSON where each line is one of these shapes. Both `createSseAdapter` and `createFetchStreamAdapter` parse this automatically — no custom `parseEvent` or `parseChunk` needed.
@@ -0,0 +1,12 @@
1
+ import type { ZodType } from 'zod';
2
+ import type { SkillDefinition, SkillSet, SkillSetOptions } from './types';
3
+ type SkillMap = Record<string, SkillDefinition<ZodType>>;
4
+ /**
5
+ * Takes a map of skill definitions and returns a SkillSet containing:
6
+ * - tools: ready-made tool definitions for ai-sdk's streamText({ tools })
7
+ * - drainActivities(): flush buffered activity wire SSE strings
8
+ * - hadFailure / failureContext: for driving tool failure recovery
9
+ */
10
+ export declare function createSkillSet(skills: SkillMap, options?: SkillSetOptions): SkillSet;
11
+ export {};
12
+ //# sourceMappingURL=createSkillSet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createSkillSet.d.ts","sourceRoot":"","sources":["../../src/server/createSkillSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;AAUzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAE,eAAoB,GAAG,QAAQ,CA8ExF"}
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function g(a){const i=a.map(t=>{const n=t.parts.filter(r=>r.type==="text").map(r=>r.text).join("");return{role:t.role,content:n}}),e=[];for(let t=0;t<i.length;t++){const n=i[t],r=e[e.length-1];n.role==="user"&&r?.role==="user"&&e.push({role:"assistant",content:"…"}),e.push(n)}return e.filter(t=>t.content.length>0)}function u(a){return`data: ${JSON.stringify(a)}
2
+
3
+ `}function f(a,i){return a[i]??a.en}function h(a,i={}){const e=i.language??"en",t=[];let n=!1,r=null;const o={};for(const[s,l]of Object.entries(a))o[s]={description:l.description,parameters:l.input,execute:async y=>{t.push(u({type:"activity",id:s,title:f(l.labels.running,e),status:"running",metadata:{skill:s}}));try{const c=await l.execute(y);return t.push(u({type:"activity",id:s,title:f(l.labels.done,e),status:"done",metadata:{skill:s}})),c}catch(c){const p=l.labels.failed??{cs:"Nepodařilo se",en:"Failed"};throw t.push(u({type:"activity",id:s,title:f(p,e),status:"failed",metadata:{skill:s}})),n=!0,r=`Tool "${s}" failed: ${c instanceof Error?c.message:String(c)}`,c}}};return{tools:o,drainActivities(){return t.splice(0,t.length)},get hadFailure(){return n},get failureContext(){return r}}}async function*d(a,i){for await(const e of a){const t=e;if(t.type==="text-delta"){if(i)for(const r of i.drainActivities())yield r;const n=t.text??t.textDelta;n&&(yield`data: ${JSON.stringify({type:"text",text:n})}
4
+
5
+ `)}}if(i)for(const e of i.drainActivities())yield e;yield`data: ${JSON.stringify({type:"done"})}
6
+
7
+ `}async function*x(a){const{phase1:i,phase2:e,skillSet:t}=a;let n=!1;const r=i();for await(const o of d(r.fullStream,t))o.includes('"type":"text"')&&(n=!0),!o.includes('"type":"done"')&&(yield o);if(t.hadFailure&&!n&&t.failureContext){const o=e(t.failureContext);yield*d(o.fullStream)}else yield`data: ${JSON.stringify({type:"done"})}
8
+
9
+ `}exports.createSkillSet=h;exports.streamToWire=d;exports.toSdkMessages=g;exports.withToolRecovery=x;
10
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/server/toSdkMessages.ts","../../src/server/createSkillSet.ts","../../src/server/streamToWire.ts","../../src/server/withToolRecovery.ts"],"sourcesContent":["import type { AiMessage } from '../core/types';\nimport type { CoreMessage } from './types';\n\n/**\n * Converts Juneau's AiMessage[] to the CoreMessage[] format expected by\n * ai-sdk's generateText / streamText.\n *\n * - Extracts plain text from parts (joins all type: \"text\" parts)\n * - Replaces activity-only assistant messages with an empty assistant turn\n * so conversation alternation stays valid\n * - Injects a placeholder assistant turn between consecutive user messages\n * - Filters out empty turns (unless they are a required alternation filler)\n */\nexport function toSdkMessages(messages: AiMessage[]): CoreMessage[] {\n // First pass: map each AiMessage to a CoreMessage, replacing activity-only\n // assistant messages with an empty-text assistant turn.\n const mapped: CoreMessage[] = messages.map(msg => {\n const text = msg.parts\n .filter(p => p.type === 'text')\n .map(p => (p as { type: 'text'; text: string }).text)\n .join('');\n\n return { role: msg.role as CoreMessage['role'], content: text };\n });\n\n // Second pass: inject a placeholder assistant turn between consecutive user\n // messages as a safety net for history bugs that would cause model errors.\n const result: CoreMessage[] = [];\n for (let i = 0; i < mapped.length; i++) {\n const current = mapped[i];\n const prev = result[result.length - 1];\n\n if (current.role === 'user' && prev?.role === 'user') {\n result.push({ role: 'assistant', content: '…' });\n }\n\n result.push(current);\n }\n\n // Third pass: filter out empty turns, but keep assistant placeholders that\n // serve as alternation fillers (they have content '…' set above, so they\n // won't be filtered). Only drop genuinely empty content strings.\n return result.filter(msg => msg.content.length > 0);\n}\n","import type { ZodType } from 'zod';\nimport type { SkillDefinition, SkillSet, SkillSetOptions } from './types';\n\ntype SkillMap = Record<string, SkillDefinition<ZodType>>;\n\nfunction formatActivityEvent(event: Record<string, unknown>): string {\n return `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nfunction pickLabel(labels: { cs: string; en: string }, language: string): string {\n return (labels as Record<string, string>)[language] ?? labels.en;\n}\n\n/**\n * Takes a map of skill definitions and returns a SkillSet containing:\n * - tools: ready-made tool definitions for ai-sdk's streamText({ tools })\n * - drainActivities(): flush buffered activity wire SSE strings\n * - hadFailure / failureContext: for driving tool failure recovery\n */\nexport function createSkillSet(skills: SkillMap, options: SkillSetOptions = {}): SkillSet {\n const language = options.language ?? 'en';\n const activityBuffer: string[] = [];\n let hadFailure = false;\n let failureContext: string | null = null;\n\n const tools: Record<string, unknown> = {};\n\n for (const [name, skill] of Object.entries(skills)) {\n tools[name] = {\n description: skill.description,\n parameters: skill.input,\n execute: async (input: unknown) => {\n // Emit a \"running\" activity event into the buffer\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(skill.labels.running, language),\n status: 'running',\n metadata: { skill: name },\n })\n );\n\n try {\n const result = await skill.execute(input as never);\n\n // Emit a \"done\" activity event\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(skill.labels.done, language),\n status: 'done',\n metadata: { skill: name },\n })\n );\n\n return result;\n } catch (err) {\n const failedLabels = skill.labels.failed ?? { cs: 'Nepodařilo se', en: 'Failed' };\n\n // Emit a \"failed\" activity event\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(failedLabels, language),\n status: 'failed',\n metadata: { skill: name },\n })\n );\n\n hadFailure = true;\n failureContext = `Tool \"${name}\" failed: ${err instanceof Error ? err.message : String(err)}`;\n\n // Re-throw so ai-sdk knows the tool call failed\n throw err;\n }\n },\n };\n }\n\n return {\n tools,\n\n drainActivities(): string[] {\n return activityBuffer.splice(0, activityBuffer.length);\n },\n\n get hadFailure() {\n return hadFailure;\n },\n\n get failureContext() {\n return failureContext;\n },\n };\n}\n","import type { SkillSet } from './types';\n\n/**\n * Converts ai-sdk's fullStream AsyncIterable into Juneau wire SSE strings.\n *\n * Handles:\n * - ai-sdk v7 chunk property rename (chunk.text) with fallback to v6 (chunk.textDelta)\n * - Flushing skillSet.drainActivities() before each text chunk\n * - A final drain after the loop ends (catches last tool call activities)\n * - Emitting the done event when the stream finishes\n */\nexport async function* streamToWire(\n fullStream: AsyncIterable<unknown>,\n skillSet?: SkillSet\n): AsyncIterable<string> {\n for await (const chunk of fullStream) {\n const c = chunk as Record<string, unknown>;\n\n if (c.type === 'text-delta') {\n // Flush any buffered activity events before emitting text\n if (skillSet) {\n for (const event of skillSet.drainActivities()) {\n yield event;\n }\n }\n\n // ai-sdk v7 uses `text`, v6 used `textDelta`\n const text = (c.text ?? c.textDelta) as string | undefined;\n if (text) {\n yield `data: ${JSON.stringify({ type: 'text', text })}\\n\\n`;\n }\n }\n\n // tool-call and tool-result chunks are handled silently — the SkillSet\n // execute fn emits activity events into its buffer which we drain above.\n }\n\n // Final drain — catches activity events from the last tool call that fired\n // after the last text-delta (or when there were no text deltas at all).\n if (skillSet) {\n for (const event of skillSet.drainActivities()) {\n yield event;\n }\n }\n\n yield `data: ${JSON.stringify({ type: 'done' })}\\n\\n`;\n}\n","import type { ToolRecoveryOptions } from './types';\nimport { streamToWire } from './streamToWire';\n\n/**\n * Encapsulates the two-phase streaming pattern needed when a tool fails and\n * the model retries the tool instead of writing text (observed with Gemini 2.5 Flash).\n *\n * Phase 1: Stream with tools, maxSteps: 1. Collect whether any text was produced.\n * Phase 2: If a tool failed and no text was produced, call the model again without\n * tools, injecting the failure context as an assistant message so the model\n * is forced to write a text response.\n *\n * @returns AsyncIterable<string> of Juneau wire SSE strings (activities + text + done)\n */\nexport async function* withToolRecovery(options: ToolRecoveryOptions): AsyncIterable<string> {\n const { phase1, phase2, skillSet } = options;\n\n let producedText = false;\n\n const result1 = phase1();\n\n for await (const chunk of streamToWire(result1.fullStream, skillSet)) {\n // Track whether any text reached the wire (done event doesn't count)\n if (chunk.includes('\"type\":\"text\"')) {\n producedText = true;\n }\n\n // Don't yield the done event yet — we may need to continue with phase 2\n if (chunk.includes('\"type\":\"done\"')) {\n continue;\n }\n\n yield chunk;\n }\n\n // If a tool failed and the model produced no text, run phase 2 to force a\n // text response without tools available.\n if (skillSet.hadFailure && !producedText && skillSet.failureContext) {\n const result2 = phase2(skillSet.failureContext);\n\n // Phase 2 has no skillSet — plain text stream, no tool calls\n yield* streamToWire(result2.fullStream);\n } else {\n // All good — emit done\n yield `data: ${JSON.stringify({ type: 'done' })}\\n\\n`;\n }\n}\n"],"names":["toSdkMessages","messages","mapped","msg","text","p","result","i","current","prev","formatActivityEvent","event","pickLabel","labels","language","createSkillSet","skills","options","activityBuffer","hadFailure","failureContext","tools","name","skill","input","err","failedLabels","streamToWire","fullStream","skillSet","chunk","c","withToolRecovery","phase1","phase2","producedText","result1","result2"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { toSdkMessages } from './toSdkMessages';
2
+ export { createSkillSet } from './createSkillSet';
3
+ export { streamToWire } from './streamToWire';
4
+ export { withToolRecovery } from './withToolRecovery';
5
+ export type { SkillSet, SkillDefinition, SkillSetOptions, ToolRecoveryOptions, CoreMessage } from './types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,116 @@
1
+ function h(a) {
2
+ const n = a.map((t) => {
3
+ const i = t.parts.filter((r) => r.type === "text").map((r) => r.text).join("");
4
+ return { role: t.role, content: i };
5
+ }), e = [];
6
+ for (let t = 0; t < n.length; t++) {
7
+ const i = n[t], r = e[e.length - 1];
8
+ i.role === "user" && r?.role === "user" && e.push({ role: "assistant", content: "…" }), e.push(i);
9
+ }
10
+ return e.filter((t) => t.content.length > 0);
11
+ }
12
+ function u(a) {
13
+ return `data: ${JSON.stringify(a)}
14
+
15
+ `;
16
+ }
17
+ function f(a, n) {
18
+ return a[n] ?? a.en;
19
+ }
20
+ function g(a, n = {}) {
21
+ const e = n.language ?? "en", t = [];
22
+ let i = !1, r = null;
23
+ const s = {};
24
+ for (const [o, l] of Object.entries(a))
25
+ s[o] = {
26
+ description: l.description,
27
+ parameters: l.input,
28
+ execute: async (p) => {
29
+ t.push(
30
+ u({
31
+ type: "activity",
32
+ id: o,
33
+ title: f(l.labels.running, e),
34
+ status: "running",
35
+ metadata: { skill: o }
36
+ })
37
+ );
38
+ try {
39
+ const c = await l.execute(p);
40
+ return t.push(
41
+ u({
42
+ type: "activity",
43
+ id: o,
44
+ title: f(l.labels.done, e),
45
+ status: "done",
46
+ metadata: { skill: o }
47
+ })
48
+ ), c;
49
+ } catch (c) {
50
+ const y = l.labels.failed ?? { cs: "Nepodařilo se", en: "Failed" };
51
+ throw t.push(
52
+ u({
53
+ type: "activity",
54
+ id: o,
55
+ title: f(y, e),
56
+ status: "failed",
57
+ metadata: { skill: o }
58
+ })
59
+ ), i = !0, r = `Tool "${o}" failed: ${c instanceof Error ? c.message : String(c)}`, c;
60
+ }
61
+ }
62
+ };
63
+ return {
64
+ tools: s,
65
+ drainActivities() {
66
+ return t.splice(0, t.length);
67
+ },
68
+ get hadFailure() {
69
+ return i;
70
+ },
71
+ get failureContext() {
72
+ return r;
73
+ }
74
+ };
75
+ }
76
+ async function* d(a, n) {
77
+ for await (const e of a) {
78
+ const t = e;
79
+ if (t.type === "text-delta") {
80
+ if (n)
81
+ for (const r of n.drainActivities())
82
+ yield r;
83
+ const i = t.text ?? t.textDelta;
84
+ i && (yield `data: ${JSON.stringify({ type: "text", text: i })}
85
+
86
+ `);
87
+ }
88
+ }
89
+ if (n)
90
+ for (const e of n.drainActivities())
91
+ yield e;
92
+ yield `data: ${JSON.stringify({ type: "done" })}
93
+
94
+ `;
95
+ }
96
+ async function* x(a) {
97
+ const { phase1: n, phase2: e, skillSet: t } = a;
98
+ let i = !1;
99
+ const r = n();
100
+ for await (const s of d(r.fullStream, t))
101
+ s.includes('"type":"text"') && (i = !0), !s.includes('"type":"done"') && (yield s);
102
+ if (t.hadFailure && !i && t.failureContext) {
103
+ const s = e(t.failureContext);
104
+ yield* d(s.fullStream);
105
+ } else
106
+ yield `data: ${JSON.stringify({ type: "done" })}
107
+
108
+ `;
109
+ }
110
+ export {
111
+ g as createSkillSet,
112
+ d as streamToWire,
113
+ h as toSdkMessages,
114
+ x as withToolRecovery
115
+ };
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/server/toSdkMessages.ts","../../src/server/createSkillSet.ts","../../src/server/streamToWire.ts","../../src/server/withToolRecovery.ts"],"sourcesContent":["import type { AiMessage } from '../core/types';\nimport type { CoreMessage } from './types';\n\n/**\n * Converts Juneau's AiMessage[] to the CoreMessage[] format expected by\n * ai-sdk's generateText / streamText.\n *\n * - Extracts plain text from parts (joins all type: \"text\" parts)\n * - Replaces activity-only assistant messages with an empty assistant turn\n * so conversation alternation stays valid\n * - Injects a placeholder assistant turn between consecutive user messages\n * - Filters out empty turns (unless they are a required alternation filler)\n */\nexport function toSdkMessages(messages: AiMessage[]): CoreMessage[] {\n // First pass: map each AiMessage to a CoreMessage, replacing activity-only\n // assistant messages with an empty-text assistant turn.\n const mapped: CoreMessage[] = messages.map(msg => {\n const text = msg.parts\n .filter(p => p.type === 'text')\n .map(p => (p as { type: 'text'; text: string }).text)\n .join('');\n\n return { role: msg.role as CoreMessage['role'], content: text };\n });\n\n // Second pass: inject a placeholder assistant turn between consecutive user\n // messages as a safety net for history bugs that would cause model errors.\n const result: CoreMessage[] = [];\n for (let i = 0; i < mapped.length; i++) {\n const current = mapped[i];\n const prev = result[result.length - 1];\n\n if (current.role === 'user' && prev?.role === 'user') {\n result.push({ role: 'assistant', content: '…' });\n }\n\n result.push(current);\n }\n\n // Third pass: filter out empty turns, but keep assistant placeholders that\n // serve as alternation fillers (they have content '…' set above, so they\n // won't be filtered). Only drop genuinely empty content strings.\n return result.filter(msg => msg.content.length > 0);\n}\n","import type { ZodType } from 'zod';\nimport type { SkillDefinition, SkillSet, SkillSetOptions } from './types';\n\ntype SkillMap = Record<string, SkillDefinition<ZodType>>;\n\nfunction formatActivityEvent(event: Record<string, unknown>): string {\n return `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nfunction pickLabel(labels: { cs: string; en: string }, language: string): string {\n return (labels as Record<string, string>)[language] ?? labels.en;\n}\n\n/**\n * Takes a map of skill definitions and returns a SkillSet containing:\n * - tools: ready-made tool definitions for ai-sdk's streamText({ tools })\n * - drainActivities(): flush buffered activity wire SSE strings\n * - hadFailure / failureContext: for driving tool failure recovery\n */\nexport function createSkillSet(skills: SkillMap, options: SkillSetOptions = {}): SkillSet {\n const language = options.language ?? 'en';\n const activityBuffer: string[] = [];\n let hadFailure = false;\n let failureContext: string | null = null;\n\n const tools: Record<string, unknown> = {};\n\n for (const [name, skill] of Object.entries(skills)) {\n tools[name] = {\n description: skill.description,\n parameters: skill.input,\n execute: async (input: unknown) => {\n // Emit a \"running\" activity event into the buffer\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(skill.labels.running, language),\n status: 'running',\n metadata: { skill: name },\n })\n );\n\n try {\n const result = await skill.execute(input as never);\n\n // Emit a \"done\" activity event\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(skill.labels.done, language),\n status: 'done',\n metadata: { skill: name },\n })\n );\n\n return result;\n } catch (err) {\n const failedLabels = skill.labels.failed ?? { cs: 'Nepodařilo se', en: 'Failed' };\n\n // Emit a \"failed\" activity event\n activityBuffer.push(\n formatActivityEvent({\n type: 'activity',\n id: name,\n title: pickLabel(failedLabels, language),\n status: 'failed',\n metadata: { skill: name },\n })\n );\n\n hadFailure = true;\n failureContext = `Tool \"${name}\" failed: ${err instanceof Error ? err.message : String(err)}`;\n\n // Re-throw so ai-sdk knows the tool call failed\n throw err;\n }\n },\n };\n }\n\n return {\n tools,\n\n drainActivities(): string[] {\n return activityBuffer.splice(0, activityBuffer.length);\n },\n\n get hadFailure() {\n return hadFailure;\n },\n\n get failureContext() {\n return failureContext;\n },\n };\n}\n","import type { SkillSet } from './types';\n\n/**\n * Converts ai-sdk's fullStream AsyncIterable into Juneau wire SSE strings.\n *\n * Handles:\n * - ai-sdk v7 chunk property rename (chunk.text) with fallback to v6 (chunk.textDelta)\n * - Flushing skillSet.drainActivities() before each text chunk\n * - A final drain after the loop ends (catches last tool call activities)\n * - Emitting the done event when the stream finishes\n */\nexport async function* streamToWire(\n fullStream: AsyncIterable<unknown>,\n skillSet?: SkillSet\n): AsyncIterable<string> {\n for await (const chunk of fullStream) {\n const c = chunk as Record<string, unknown>;\n\n if (c.type === 'text-delta') {\n // Flush any buffered activity events before emitting text\n if (skillSet) {\n for (const event of skillSet.drainActivities()) {\n yield event;\n }\n }\n\n // ai-sdk v7 uses `text`, v6 used `textDelta`\n const text = (c.text ?? c.textDelta) as string | undefined;\n if (text) {\n yield `data: ${JSON.stringify({ type: 'text', text })}\\n\\n`;\n }\n }\n\n // tool-call and tool-result chunks are handled silently — the SkillSet\n // execute fn emits activity events into its buffer which we drain above.\n }\n\n // Final drain — catches activity events from the last tool call that fired\n // after the last text-delta (or when there were no text deltas at all).\n if (skillSet) {\n for (const event of skillSet.drainActivities()) {\n yield event;\n }\n }\n\n yield `data: ${JSON.stringify({ type: 'done' })}\\n\\n`;\n}\n","import type { ToolRecoveryOptions } from './types';\nimport { streamToWire } from './streamToWire';\n\n/**\n * Encapsulates the two-phase streaming pattern needed when a tool fails and\n * the model retries the tool instead of writing text (observed with Gemini 2.5 Flash).\n *\n * Phase 1: Stream with tools, maxSteps: 1. Collect whether any text was produced.\n * Phase 2: If a tool failed and no text was produced, call the model again without\n * tools, injecting the failure context as an assistant message so the model\n * is forced to write a text response.\n *\n * @returns AsyncIterable<string> of Juneau wire SSE strings (activities + text + done)\n */\nexport async function* withToolRecovery(options: ToolRecoveryOptions): AsyncIterable<string> {\n const { phase1, phase2, skillSet } = options;\n\n let producedText = false;\n\n const result1 = phase1();\n\n for await (const chunk of streamToWire(result1.fullStream, skillSet)) {\n // Track whether any text reached the wire (done event doesn't count)\n if (chunk.includes('\"type\":\"text\"')) {\n producedText = true;\n }\n\n // Don't yield the done event yet — we may need to continue with phase 2\n if (chunk.includes('\"type\":\"done\"')) {\n continue;\n }\n\n yield chunk;\n }\n\n // If a tool failed and the model produced no text, run phase 2 to force a\n // text response without tools available.\n if (skillSet.hadFailure && !producedText && skillSet.failureContext) {\n const result2 = phase2(skillSet.failureContext);\n\n // Phase 2 has no skillSet — plain text stream, no tool calls\n yield* streamToWire(result2.fullStream);\n } else {\n // All good — emit done\n yield `data: ${JSON.stringify({ type: 'done' })}\\n\\n`;\n }\n}\n"],"names":["toSdkMessages","messages","mapped","msg","text","p","result","i","current","prev","formatActivityEvent","event","pickLabel","labels","language","createSkillSet","skills","options","activityBuffer","hadFailure","failureContext","tools","name","skill","input","err","failedLabels","streamToWire","fullStream","skillSet","chunk","c","withToolRecovery","phase1","phase2","producedText","result1","result2"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { SkillSet } from './types';
2
+ /**
3
+ * Converts ai-sdk's fullStream AsyncIterable into Juneau wire SSE strings.
4
+ *
5
+ * Handles:
6
+ * - ai-sdk v7 chunk property rename (chunk.text) with fallback to v6 (chunk.textDelta)
7
+ * - Flushing skillSet.drainActivities() before each text chunk
8
+ * - A final drain after the loop ends (catches last tool call activities)
9
+ * - Emitting the done event when the stream finishes
10
+ */
11
+ export declare function streamToWire(fullStream: AsyncIterable<unknown>, skillSet?: SkillSet): AsyncIterable<string>;
12
+ //# sourceMappingURL=streamToWire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamToWire.d.ts","sourceRoot":"","sources":["../../src/server/streamToWire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;;;;;;GAQG;AACH,wBAAuB,YAAY,CACjC,UAAU,EAAE,aAAa,CAAC,OAAO,CAAC,EAClC,QAAQ,CAAC,EAAE,QAAQ,GAClB,aAAa,CAAC,MAAM,CAAC,CAgCvB"}
@@ -0,0 +1,14 @@
1
+ import type { AiMessage } from '../core/types';
2
+ import type { CoreMessage } from './types';
3
+ /**
4
+ * Converts Juneau's AiMessage[] to the CoreMessage[] format expected by
5
+ * ai-sdk's generateText / streamText.
6
+ *
7
+ * - Extracts plain text from parts (joins all type: "text" parts)
8
+ * - Replaces activity-only assistant messages with an empty assistant turn
9
+ * so conversation alternation stays valid
10
+ * - Injects a placeholder assistant turn between consecutive user messages
11
+ * - Filters out empty turns (unless they are a required alternation filler)
12
+ */
13
+ export declare function toSdkMessages(messages: AiMessage[]): CoreMessage[];
14
+ //# sourceMappingURL=toSdkMessages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toSdkMessages.d.ts","sourceRoot":"","sources":["../../src/server/toSdkMessages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,WAAW,EAAE,CA8BlE"}
@@ -0,0 +1,67 @@
1
+ import type { ZodType, z } from 'zod';
2
+ /**
3
+ * A single skill definition — description, zod input schema, i18n labels, and the execute fn.
4
+ */
5
+ export interface SkillDefinition<TInput extends ZodType = ZodType> {
6
+ description: string;
7
+ input: TInput;
8
+ labels: {
9
+ running: {
10
+ cs: string;
11
+ en: string;
12
+ };
13
+ done: {
14
+ cs: string;
15
+ en: string;
16
+ };
17
+ failed?: {
18
+ cs: string;
19
+ en: string;
20
+ };
21
+ };
22
+ execute: (input: z.infer<TInput>) => Promise<unknown>;
23
+ }
24
+ /**
25
+ * The object returned by createSkillSet.
26
+ */
27
+ export interface SkillSet {
28
+ /** ai-sdk ToolSet — pass directly to streamText({ tools }) */
29
+ tools: Record<string, unknown>;
30
+ /**
31
+ * Returns and clears all buffered activity wire SSE strings.
32
+ * Call before yielding text chunks to flush running/done/failed indicators.
33
+ */
34
+ drainActivities(): string[];
35
+ /**
36
+ * True if any tool execution failed during the last call.
37
+ * Use to decide whether to run the failure recovery phase.
38
+ */
39
+ hadFailure: boolean;
40
+ /**
41
+ * Human-readable failure context for the recovery prompt.
42
+ * Set after a failed tool call. Use as an assistant message in the follow-up call.
43
+ */
44
+ failureContext: string | null;
45
+ }
46
+ export interface SkillSetOptions {
47
+ /** Language code — determines which label variant to use. Default: 'en' */
48
+ language?: string;
49
+ }
50
+ export interface ToolRecoveryOptions {
51
+ phase1: () => {
52
+ fullStream: AsyncIterable<unknown>;
53
+ };
54
+ phase2: (failureContext: string) => {
55
+ fullStream: AsyncIterable<unknown>;
56
+ };
57
+ skillSet: SkillSet;
58
+ }
59
+ /**
60
+ * CoreMessage — structural match for ai-sdk's CoreMessage type.
61
+ * Juneau does not import ai-sdk; this plain object is compatible by structure.
62
+ */
63
+ export type CoreMessage = {
64
+ role: 'user' | 'assistant' | 'system';
65
+ content: string;
66
+ };
67
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,MAAM,SAAS,OAAO,GAAG,OAAO;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE;QACN,OAAO,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QACpC,IAAI,EAAK;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QACpC,MAAM,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;KACrC,CAAC;IACF,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE/B;;;OAGG;IACH,eAAe,IAAI,MAAM,EAAE,CAAC;IAE5B;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM;QAAE,UAAU,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IACrD,MAAM,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK;QAAE,UAAU,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAC3E,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ToolRecoveryOptions } from './types';
2
+ /**
3
+ * Encapsulates the two-phase streaming pattern needed when a tool fails and
4
+ * the model retries the tool instead of writing text (observed with Gemini 2.5 Flash).
5
+ *
6
+ * Phase 1: Stream with tools, maxSteps: 1. Collect whether any text was produced.
7
+ * Phase 2: If a tool failed and no text was produced, call the model again without
8
+ * tools, injecting the failure context as an assistant message so the model
9
+ * is forced to write a text response.
10
+ *
11
+ * @returns AsyncIterable<string> of Juneau wire SSE strings (activities + text + done)
12
+ */
13
+ export declare function withToolRecovery(options: ToolRecoveryOptions): AsyncIterable<string>;
14
+ //# sourceMappingURL=withToolRecovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withToolRecovery.d.ts","sourceRoot":"","sources":["../../src/server/withToolRecovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGnD;;;;;;;;;;GAUG;AACH,wBAAuB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,aAAa,CAAC,MAAM,CAAC,CAgC3F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juneau",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "description": "Open-source React/TypeScript library for AI chat UI components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -12,6 +12,11 @@
12
12
  "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
14
  },
15
+ "./server": {
16
+ "types": "./dist/server/index.d.ts",
17
+ "import": "./dist/server/index.js",
18
+ "require": "./dist/server/index.cjs"
19
+ },
15
20
  "./dist/style.css": "./dist/style.css"
16
21
  },
17
22
  "files": [
@@ -22,7 +27,7 @@
22
27
  ],
23
28
  "scripts": {
24
29
  "dev": "vite",
25
- "build": "npm run build:types && vite build --mode lib",
30
+ "build": "npm run build:types && vite build --mode lib && vite build --config vite.config.server.ts",
26
31
  "build:types": "tsc --project tsconfig.build.json",
27
32
  "build:demo": "vite build",
28
33
  "preview": "vite preview --outDir dist-demo",
@@ -38,7 +43,8 @@
38
43
  },
39
44
  "dependencies": {
40
45
  "react-markdown": "^10.1.0",
41
- "remark-gfm": "^4.0.1"
46
+ "remark-gfm": "^4.0.1",
47
+ "zod": "^4.4.3"
42
48
  },
43
49
  "devDependencies": {
44
50
  "@chromatic-com/storybook": "latest",