juneau 0.7.0 → 0.7.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
@@ -187,7 +187,7 @@ export const myAdapter: AiBackendAdapter = {
187
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
188
 
189
189
  ```ts
190
- import { toSdkMessages, createSkillSet, buildSkillIndex, selectSkills, withToolRecovery } from 'juneau/server';
190
+ import { toSdkMessages, createSkillSet, buildSkillIndex, selectSkills, selectSkillsById, withToolRecovery, withSkillDispatch } from 'juneau/server';
191
191
  ```
192
192
 
193
193
  **Full integration in ~15 lines:**
@@ -240,29 +240,72 @@ execute: async ({ query }, { emit }) => {
240
240
 
241
241
  Emitted parts share the activity buffer and are drained by `streamToWire` at the same points — they appear in the stream in emit order, before the model's text response.
242
242
 
243
- Skill metadata: `title` (human-readable name), `instructions?` (agent workflow text — lazily loaded via `selectSkills`), `readOnly?` (default `true`), `requiresConfirmation?` (default `false`), `tools?` (skill composition — validated at startup, `createSkillSet` throws on a reference to an unknown skill).
243
+ Skill metadata: `title` (human-readable name), `id` (numeric, required — chosen by the consumer, must be unique across the skill map; `createSkillSet` throws at startup on duplicates), `instructions?` (agent workflow text — lazily loaded via `selectSkills` / `selectSkillsById`), `readOnly?` (default `true`), `requiresConfirmation?` (default `false`), `tools?` (skill composition — validated at startup, `createSkillSet` throws on a reference to an unknown skill).
244
244
 
245
- `SkillSet` members: `tools`, `skills`, `calledSkillNames`, `drainActivities()`, `hadFailure`, `failureContext`.
245
+ `SkillSet` members: `tools`, `skills`, `skillsById` (Map<number, SkillDefinition>), `calledSkillNames`, `calledSkillIds`, `drainActivities()`, `hadFailure`, `failureContext`.
246
246
 
247
247
  `SkillSetOptions`: `language?` — selects label variant (`'en'` default). `debug?` — emit `console.debug` logs per skill execution (default: `false`).
248
248
 
249
249
  #### `buildSkillIndex(skillSet)`
250
250
 
251
- Generates a compact one-liner-per-skill index for the system prompt — registry-driven, so the prompt never drifts from the actual skills:
251
+ Generates a compact one-liner-per-skill index for the system prompt — registry-driven, so the prompt never drifts from the actual skills. Each line includes a numeric ID so the model can request skills by ID rather than by name:
252
252
 
253
253
  ```
254
- - invoiceSearch (read): Find invoices by number, supplier, date, or status.
255
- - invoiceApprove (write, requires confirmation): Approve an invoice.
254
+ [1] invoiceSearch (read): Find invoices by number, supplier, date, or status.
255
+ [2] invoiceApprove (write, requires confirmation): Approve an invoice.
256
256
  ```
257
257
 
258
258
  #### `selectSkills(skillSet, skillNames)`
259
259
 
260
- Returns concatenated `instructions` for the given skill names — typically `skillSet.calledSkillNames` after phase 1, injected into phase 3's system prompt. Workflow instructions load lazily, only for skills the model actually used.
260
+ Returns concatenated `instructions` for the given skill names — typically `skillSet.calledSkillNames` after phase 1. Workflow instructions load lazily, only for skills the model actually used.
261
+
262
+ #### `selectSkillsById(skillSet, skillIds)`
263
+
264
+ Same as `selectSkills` but resolves by numeric ID — use with `skillSet.calledSkillIds` to avoid string comparisons entirely. Unknown IDs and skills without instructions are skipped silently.
261
265
 
262
266
  #### `streamToWire(fullStream, skillSet?, options?)`
263
267
 
264
268
  Converts ai-sdk `fullStream` to Juneau wire SSE strings. Handles all ai-sdk v7 text chunk types (`text-delta` and `text`), drains activity buffer at the right moment, emits `done` at the end. Pass `{ debug: true }` to log every chunk received from ai-sdk.
265
269
 
270
+ #### `withSkillDispatch(options)`
271
+
272
+ Deliberate 2-phase skill dispatch — the clean alternative to sending every skill's full instructions on every request.
273
+
274
+ **Phase 1:** stream with `buildSkillIndex` as the system prompt and thin tool definitions. The model picks a skill by calling a tool — `calledSkillIds` is populated as tools fire.
275
+
276
+ **Phase 2:** always runs when at least one skill was called. Receives the full workflow instructions for the called skills (via `selectSkillsById`) and the complete phase 1 message history including tool-call and tool-result turns. The model is called again without tools so it reads the instructions and produces a text response.
277
+
278
+ If no skill was called (model answered directly), the phase 1 text is emitted as-is and the stream ends cleanly — no phase 2 needed.
279
+
280
+ ```ts
281
+ for await (const chunk of withSkillDispatch({
282
+ phase1: () => streamText({
283
+ model,
284
+ system: buildSkillIndex(skillSet), // compact index: [1] search (read): ...
285
+ messages: sdkMessages,
286
+ tools: skillSet.tools,
287
+ maxSteps: 1,
288
+ }),
289
+ phase2: (instructions, history) => streamText({
290
+ model,
291
+ system: instructions, // full workflow text for the chosen skill only
292
+ messages: history, // full phase 1 history incl. tool-call + tool-result
293
+ }),
294
+ skillSet,
295
+ onFinish: ({ text }) => saveAssistantReply(text),
296
+ })) {
297
+ res.write(chunk);
298
+ }
299
+ ```
300
+
301
+ | Option | Type | Description |
302
+ |---|---|---|
303
+ | `phase1` | `() => ToolRecoveryStreamResult` | Phase 1 stream — model picks a skill via tool call |
304
+ | `phase2` | `(instructions, messages) => { fullStream }` | Phase 2 stream — model executes with full instructions |
305
+ | `skillSet` | `SkillSet` | The skill set used in phase 1 |
306
+ | `onFinish` | `({ text }) => void` | Called once before `done` with accumulated text. Not called on error paths. |
307
+ | `debug` | `boolean` | Log phase decisions to `console.debug`. Default: `false`. |
308
+
266
309
  #### `withToolRecovery(options)`
267
310
 
268
311
  Multi-phase pattern for Gemini-style tool calls. Phase 1 streams with tools. Phase 2 triggers only when a tool failed **and** no text was produced — it calls the model without tools and injects the failure context to force a text response. Phase 3 (optional) handles the silent-success case — Gemini 2.5 Flash often treats a successful tool call as its complete response and never writes text. When the tool succeeded but no text was produced, `phase3` receives the full phase 1 message history (resolved from the ai-sdk result's `messages` promise — includes tool-call and tool-result turns, which Gemini requires to accept the history) so a second model call without tools can summarise the result. Errors thrown by phase 2/3 are emitted as wire `error` events instead of a silent `done`. Pass `debug: true` to log phase decisions, the resolved phase 3 history, and the `textProduced` / `hadFailure` state at each decision point.
@@ -283,6 +326,8 @@ yield* withToolRecovery({
283
326
 
284
327
  The wire/message types shared with the client — `AiMessage`, `AiMessageRole`, `AiMessagePart`, `AiTextPart`, `AiSerializedMessage`, `AiStreamEvent`, `JuneauWireEvent` (and its member types) — are also re-exported from `juneau/server`, so backend code never needs to import from the client entry.
285
328
 
329
+ Server-only types: `SkillSet`, `SkillDefinition`, `SkillExecuteContext`, `SkillSetOptions`, `SkillDispatchOptions`, `ToolRecoveryOptions`, `ToolRecoveryStreamResult`, `StreamToWireOptions`, `CoreMessage`.
330
+
286
331
  ---
287
332
 
288
333
  ### Juneau wire protocol — for Juneau-compatible backends