Skip to content

feat: skills subsystem + authoring wizard in @sqlrooms/ai - #574

Merged
ilyabo merged 13 commits into
mainfrom
mihajlo/skills-and-wizard
May 18, 2026
Merged

ilyabo merged 13 commits into
mainfrom
mihajlo/skills-and-wizard

Conversation

@mihajlo-stojkovic

@mihajlo-stojkovic mihajlo-stojkovic commented Apr 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a first-class skills subsystem to @sqlrooms/ai: a storage abstraction for discovering/persisting skills, a split manifest format, and a prompt-to-skill authoring wizard host apps can drop into a dialog. Also lands an AgentChat primitive in @sqlrooms/ai-core for transient, session-less agent conversations (the wizard's chat surface), and wires the whole pattern into examples/ai-agent end-to-end.

Motivation

Host apps that ship pre-canned "skills" to their agents have had to roll their own everything: storage layout, manifest parsing, an editor, and any kind of authoring UX. The pieces are generic enough to live in the library — only the actual storage backend (filesystem, HTTP, OPFS, IndexedDB…) is host-specific.

This PR introduces the generic primitives and a reference implementation so a consumer only has to swap in their storage implementation and a capability surface.

What's in @sqlrooms/ai/skills

Manifest + storage

  • Split format: skill.yaml (Zod-validated metadata: id, version, name, description, optional author/icon) next to a SKILL.md body containing LLM instructions. No frontmatter inside the markdown.
  • parseSkillManifest / serializeSkillManifest / loadSkillFromFiles — YAML + Zod parsing with structured, serializable errors (SkillManifestError, SkillNotFoundError, SkillRootReadOnlyError, SkillConflictError).
  • SkillStorage interface — listRoots, listSkills, readSkill, writeSkill, deleteSkill, resolveSkillId. Supports multiple prioritized roots (e.g. installed > user > built-in) for conflict resolution. No fs/Node imports; the interface is backend-agnostic.

Authoring wizard

  • createSkillAuthoringAgent({model, context, draftStore, onSave}) — a ToolLoopAgent factory with three tools:
    • writeManifest({name, description, author?})
    • writeInstructions({markdown})
    • saveSkill({rootId?})
  • createSkillDraftStore() — per-session Zustand store (Immer) tracking the live draft + status. Fresh instance per wizard opening; no global singleton.
  • SkillAuthoringContext — the host-supplied capability surface the agent may reference: services[], tools[], permissions[], plus forbiddenIdentifiers[] for portability checks. The wizard stays domain-neutral; hosts fill in their own vocabulary.
  • buildSkillAuthoringSystemPrompt(context) — deterministic system prompt covering role, tools, capability surface, a portability rule (no hardcoded table/column names), and compact skill.yaml / SKILL.md examples.
  • SkillAuthoringPanel — split-view React component: AgentChat on the left, live SkillDraftPreview (structured manifest card + rendered markdown) on the right. Knows nothing about filesystems, IPC, or any specific LLM provider — the caller owns the agent and the draft store.

Tool validators reject inputs matching any forbiddenIdentifiers token via word-boundary, case-insensitive match. This nudges the agent toward portable templates that run against whatever data the user has.

@sqlrooms/ai-core — AgentChat

A new domain-neutral chat primitive driven by an externally constructed ToolLoopAgent. Unlike the session-based Chat component, AgentChat owns no session state, no AI slice, no model selection, no persistence — built for transient, single-purpose agent conversations (authoring wizards, inline approvals, one-shot queries).

Reuses FlatAgentRenderer + ToolRenderBehavior so the tool-call rendering stays consistent with the main chat surface.

Example app (examples/ai-agent)

Demonstrates the three extension points a consumer swaps:

  • InMemorySkillStorage — reference SkillStorage impl backed by Maps. Two roots (read-only built-in, writable default). Seeds round-trip through parseSkillManifest so schema drift fails loudly on load.
  • SkillsButton — header button that opens a Dialog containing SkillAuthoringPanel. Fresh agent + draft store per open; save flushes into InMemorySkillStorage and refreshes the listing.
  • runSkillTool — minimal runtime: resolve {skillId} → read record → spin up a fresh ToolLoopAgent with the skill's SKILL.md as instructions and querySQL as its only tool → stream via streamSubAgent so tool calls surface in the parent's activity log.

README has a Skills section walking through storage / authoring / runtime.

Notes

  • No back-compat layer: skills use the split format from day one.
  • Authoring sessions are ephemeral; drafts are discarded on dialog close.
  • yaml added as a dependency of @sqlrooms/ai.
  • Two small chores ride along: opt @sqlrooms/deck out of ambient @types in its tsconfig (unblocks typecheck), and drop an unused @loaders.gl/core dep.

Test plan

  • pnpm typecheck at repo root — no errors
  • pnpm --filter @sqlrooms/ai test — manifest schema + authoring-tool unit tests pass
  • pnpm dev ai-agent-example — open New skill, describe a skill, watch the preview populate, save, and run it from chat
  • Existing FlatAgentRenderer / Chat usages render unchanged

Summary by CodeRabbit

  • New Features

    • Full AI "Skills" subsystem: authoring UI/panel, per-draft store, authoring agent/tools, in-memory and composite storage, seeded example skills, skill-run and simulated SQL tools, Skills header button, and dynamic model selection from app settings.
    • AgentChat UI: message rendering, streaming-aware chat input (stop/send), suggestion strip, tool-activity lines, sticky-scroll and transport hooks, and exported tool-render behavior.
  • Documentation

    • Added docs and examples for Skills subsystem, AgentChat usage, and example project local run (pnpm) with OPENAI_API_KEY requirement.

Review Change Stack

Set `types: []` in packages/deck/tsconfig.json so TypeScript does not
automatically include every `@types/*` package found by walking up the
node_modules tree. Explicit imports in source are unaffected.

This avoids collisions when a legacy ambient deck.gl type declaration
exists higher up in the module resolution path, which can shadow the
real `@deck.gl/core` types and break named exports such as `DeckProps`.
@coderabbitai

coderabbitai Bot commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a skills subsystem (manifest parsing/validation, storage interfaces and implementations, composite storage, error types), an authoring flow (draft store, authoring tools, agent, UI), runtime skill execution (querySQL simulation, runSkill), AgentChat UI components, example app wiring with seeded skills/in-memory storage, and README/package/tsconfig updates.

Changes

Skills Subsystem and Agent Chat UI

Layer / File(s) Summary
Skill manifest parsing & serialization
packages/ai/src/skills/manifest.ts
SkillManifest schema, parseSkillManifest, serializeSkillManifest, and loadSkillFromFiles for YAML-backed skill files.
Skill types & storage interface
packages/ai/src/skills/storage.ts
Public types: SkillRoot, SkillRef, SkillRecord, SkillListing, SkillWriteContent and SkillStorage interface (adds optional subscribe).
Skill error types
packages/ai/src/skills/errors.ts
Structured SkillError base and subclasses (SkillManifestError, SkillNotFoundError, SkillRootReadOnlyError, SkillConflictError) with JSON serialization.
Composite storage
packages/ai/src/skills/composite.ts
CompositeSkillStorage that composes child storages with priority-first dispatch and optional subscribe fan-out.
Authoring draft store & tools
packages/ai/src/skills/authoring/createSkillDraftStore.ts, packages/ai/src/skills/authoring/tools.ts
Zustand draft store plus tools: createWriteManifestTool, createWriteInstructionsTool, createSaveSkillTool; forbidden-identifier enforcement and status/error handling.
Authoring agent & system prompt
packages/ai/src/skills/authoring/SkillAuthoringAgent.ts, packages/ai/src/skills/authoring/systemPrompt.ts, packages/ai/src/skills/authoring/SkillAuthoringContext.ts
createSkillAuthoringAgent wires authoring tools into a ToolLoopAgent using a deterministic system prompt and configurable stop steps/temperature.
Authoring UI
packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx, packages/ai/src/skills/authoring/SkillDraftPreview.tsx
Split-pane UI with AgentChat on the left driving the authoring agent and live preview/status on the right; exports header and panel types.
AgentChat core components
packages/ai-core/src/components/agent-chat/*.tsx, packages/ai-core/src/components/FlatAgentRenderer.tsx
AgentChat, ChatInput, MessageView, SuggestionStrip, ToolActivityLine, partToAgentToolCall, exported useToolRenderBehavior, and supporting hooks.
Chat transport & utilities
packages/ai-core/src/components/agent-chat/useAgentChatTransport.ts, packages/ai-core/src/components/agent-chat/useStickyScroll.ts
Transport that drives agents locally (createAgentUIStreamResponse) and sticky-scroll hook to keep view pinned when appropriate.
Example: Seeded skills
examples/ai-agent/src/skills/seedSkills.ts
Three built-in seed skills (summarize-table, plot-column, find-duplicates) with manifests and instructions referencing querySQL.
Example: In-memory storage
examples/ai-agent/src/skills/InMemorySkillStorage.ts
InMemorySkillStorage with two roots (writable default, read-only built-in), seeding, subscribe/notify, CRUD, validation, and id resolution.
Example: Runtime tools
examples/ai-agent/src/skills/getModel.ts, examples/ai-agent/src/skills/querySqlTool.ts, examples/ai-agent/src/skills/runSkillTool.ts
getModel resolves LLM from store; querySQL simulates a 3-row result; runSkill executes a skill as a sub-agent and streams tool calls into parent log.
Example wiring & UI
examples/ai-agent/src/components/SkillsButton.tsx, examples/ai-agent/src/components/MainView.tsx, examples/ai-agent/src/store.ts, examples/ai-agent/src/agents/WeatherAgent.ts
SkillsButton opens authoring dialog and saves via skillStorage; MainView renders button area; store exports skillStorage, caches listings, builds prompts from listings, and wires runSkill; WeatherAgent refactored to use getModel.
Package exports & deps
packages/ai/src/index.ts, packages/ai-core/src/index.ts, packages/ai/package.json, examples/ai-agent/package.json, packages/deck/package.json, packages/deck/tsconfig.json
Exports skill APIs and AgentChat; adds dependencies (yaml, zustand, immer, react-markdown, remark-gfm), updates example deps to include @sqlrooms/ai, removes unused deck dependency, and tweaks deck tsconfig.
Documentation & READMEs
examples/ai-agent/README.md, packages/ai/README.md, packages/ai-core/README.md
README updates: example run instructions (pnpm, OPENAI_API_KEY) and new "Skills" / "AgentChat" documentation sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sqlrooms/sqlrooms#497: Related changes to agent utilities and streaming that the new runSkill runtime relies on.

Suggested reviewers

  • ilyabo
  • lixun910
  • dmitriy-kostianetskiy

"A rabbit wrote a skill in YAML bright,
hopped through agents, and made UIs light.
Seeded skills, a chat that streams,
authorship powered by clever dreams. 🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: skills subsystem + authoring wizard in @sqlrooms/ai' accurately captures the main changes: introducing a new skills subsystem with manifest format, storage interface, and an authoring wizard to the @sqlrooms/ai package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mihajlo/skills-and-wizard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mihajlo-stojkovic
mihajlo-stojkovic marked this pull request as ready for review May 7, 2026 13:50
- Fix IPC serialization: replace open index signature in SkillErrorContext with JsonValue-constrained extras field
- Fix type safety: make SkillAuthoringContext array fields readonly to prevent post-construction mutation
- Fix efficiency: rewrite loadSkillFromFiles to single-pass, pre-compile forbidden identifier regexes in tools
- Fix UX bug: useStickyScroll now gates auto-scroll on wasAtBottom to avoid interrupting user scroll
- Fix silent failures: add error logging to parseUiMessages, refreshSkillListings, runSkillTool execute
- Fix redundant state: remove Immer from flat createSkillDraftStore setters, use plain Zustand set
- Fix duplicate code: extract checkForbidden helper in tools.ts, use existing isToolPart/isDynamicToolPart guards
- Fix InMemorySkillStorage: path-traversal guard, correct error type for unknown root, remove double validation, cache parsed manifests
- Fix example: deduplicate getModel in WeatherAgent, remove redundant nonce state in SkillsButton, fix gpt-4.1 fallback model
- Export SKILL_AUTHORING_TOOL_NAMES constants; replace string literals in SkillAuthoringPanel
- Add Skills and AgentChat sections to @sqlrooms/ai and @sqlrooms/ai-core READMEs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
packages/ai-core/src/components/agent-chat/MessageView.tsx (1)

8-8: 💤 Low value

Inline or drop the empty MARKDOWN_COMPONENTS constant.

MARKDOWN_COMPONENTS is {} and only used once. Either drop the prop on ReactMarkdown (default behavior is identical) or inline it; the named constant suggests there's customization here when there isn't. If it's a placeholder for future overrides, consider a brief comment to that effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-core/src/components/agent-chat/MessageView.tsx` at line 8, The
empty MARKDOWN_COMPONENTS constant in MessageView.tsx is unused and misleading;
remove the constant and its use by deleting MARKDOWN_COMPONENTS and removing the
components={MARKDOWN_COMPONENTS} prop from the ReactMarkdown invocation
(ReactMarkdown defaults are sufficient), or if you want to keep a placeholder,
inline {} directly as components={{}} on the ReactMarkdown call and add a brief
comment indicating it's intentional for future overrides; update the
ReactMarkdown usage accordingly.
examples/ai-agent/src/store.ts (1)

27-49: ⚖️ Poor tradeoff

Consider hoisting skillStorage and listings into the store.

Per coding guidelines, app-level state that needs to coordinate across panels is preferably modeled in the room Zustand store rather than module-level singletons. Putting skillStorage and cachedListings on a slice would also let getInstructions read from get() instead of a closed-over module variable, and keeps lifecycle/teardown explicit. Current setup works for the example but is something to keep in mind if this graduates beyond the demo.

As per coding guidelines: "Use the room Zustand store when state is app-level, cross-panel, or needs to coordinate with other room features".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ai-agent/src/store.ts` around lines 27 - 49, Hoist the module-level
skillStorage and cachedListings into the room Zustand store by adding them to a
slice (e.g., include skillStorage: InMemorySkillStorage and cachedListings:
SkillListing[] in the slice state and a refreshSkillListings action), initialize
skillStorage inside the slice constructor, call refreshSkillListings once after
creation, and subscribe to skillStorage within the slice to update
cachedListings via set(); then update any callers (for example getInstructions)
to read cachedListings from get() instead of the closed-over module variable so
lifecycle and teardown are managed by the store.
examples/ai-agent/src/components/SkillsButton.tsx (1)

87-91: 💤 Low value

Bound the collision-resolution loop.

while (true) is safe in practice (each iteration increments counter, and slugify always returns a non-empty base), but a defensive cap avoids an unbounded await loop should resolveSkillId ever incorrectly return truthy for unknown IDs. A small MAX_ATTEMPTS with a thrown error is cheap insurance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ai-agent/src/components/SkillsButton.tsx` around lines 87 - 91, The
collision-resolution loop using while(true) must be bounded; replace it with a
capped loop (e.g., define a MAX_ATTEMPTS constant and iterate up to that many
times while calling skillStorage.resolveSkillId to check candidateId),
incrementing counter and updating candidateId as now, and if MAX_ATTEMPTS is
exceeded throw a clear error (or reject) mentioning the baseId and that unique
ID allocation failed; update the code around candidateId, baseId, counter and
the call to skillStorage.resolveSkillId to use this bounded approach.
packages/ai/src/skills/storage.ts (1)

69-117: ⚖️ Poor tradeoff

Consider adding subscribe to the SkillStorage interface for UI reactivity.

InMemorySkillStorage exposes a subscribe(listener) → unsubscribe method that the example app UI depends on for reactive listings after writeSkill/deleteSkill. Because subscribe is not on the interface, any component that calls it is implicitly coupled to the concrete InMemorySkillStorage type rather than the SkillStorage abstraction. If other backends (OPFS, HTTP) are expected to drive reactive UIs, elevating subscribe to the interface would keep callers backend-agnostic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/skills/storage.ts` around lines 69 - 117, The SkillStorage
interface lacks a subscribe/unsubscribe API which InMemorySkillStorage currently
exposes, causing consumers to depend on the concrete InMemorySkillStorage
instead of the abstraction; add a subscribe method to the SkillStorage interface
(e.g., subscribe(listener: () => void): () => void) so callers can register a
listener that is invoked after mutations (writeSkill/deleteSkill) and receive an
unsubscribe function, and then implement/forward that behavior in
InMemorySkillStorage (and any other backends) so UI code can remain
backend-agnostic.
packages/ai-core/src/components/agent-chat/index.tsx (1)

139-150: 💤 Low value

useMessagesObserver fires on mount — consider clarifying the JSDoc.

The effect at line 147 fires on initial mount with messages = initialMessages ?? [], so onMessagesChange is called immediately even before any user interaction. The comment says "on every stream delta" but the actual behavior is "on every message-array change, including mount." This won't break anything (consumers tolerating redundant calls is safe), but the JSDoc is slightly misleading for hosts that only want post-send updates.

📝 Proposed JSDoc clarification
-/**
- * Fire `onMessagesChange` on every message delta without re-subscribing when
- * the callback identity churns. Ref-guarded so hosts can pass inline
- * functions without triggering redundant calls.
- */
+/**
+ * Fire `onMessagesChange` whenever the messages array reference changes,
+ * including on initial mount with the seed messages. Ref-guarded so hosts
+ * can pass inline functions without triggering redundant calls.
+ */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-core/src/components/agent-chat/index.tsx` around lines 139 - 150,
Update the JSDoc for useMessagesObserver to accurately state that the effect
runs on initial mount and whenever the messages array changes (not just "stream
deltas"), so onMessagesChange will be invoked immediately with the initial
messages (initialMessages ?? []) and on every subsequent messages array update;
reference the function name useMessagesObserver and its parameters (messages,
onMessagesChange) in the comment so consumers know this includes the initial
render and can opt out if they only want post-send updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/ai-agent/src/skills/InMemorySkillStorage.ts`:
- Around line 128-146: The extraFiles loop must also guard against reserved
filenames to prevent duplicates; update the loop in InMemorySkillStorage (where
content.extraFiles is iterated) to throw a SkillManifestError (including
{rootId}) if any file.relativePath equals 'skill.yaml' or 'SKILL.md' (avoid
case/whitespace surprises by normalizing, e.g., trim and toLowerCase) before
constructing the files array that contains
serializeSkillManifest(content.manifest) and SKILL.md so reserved entries cannot
be shadowed.

In `@examples/ai-agent/src/store.ts`:
- Around line 34-49: The current refreshSkillListings() suffers from race
conditions that let out-of-order async calls overwrite cachedListings; change
refreshSkillListings to use a monotonic sequence token (e.g., incrementing
requestId) captured per invocation and only assign cachedListings if the token
matches the latest seen token, and ensure the initial call is awaited (or its
token respected) so any prompt build sees up-to-date data; update the subscribe
handler to call the guarded refreshSkillListings() (and not fire-and-forget
without sequencing) so only the most recent fetch wins and stale results are
discarded.

In `@packages/ai-core/README.md`:
- Around line 105-118: The README example incorrectly uses the non-existent
createToolLoopAgent factory; replace its usage with instantiation of the
ToolLoopAgent class by using new ToolLoopAgent(...) and pass the same config
(model, system, tools) as the constructor argument; update the import/examples
to reference ToolLoopAgent instead of createToolLoopAgent so the sample compiles
with ai v6.

In `@packages/ai-core/src/components/agent-chat/ChatInput.tsx`:
- Around line 26-33: The Enter key handler (handleKeyDown) currently calls
onSend(value) unconditionally and bypasses the same guards the send button uses;
update handleKeyDown (and the other Enter handler around the 51-56 block) to
perform the same checks as the send button: return early if isStreaming is true
or if the input is empty/whitespace, and only call onSend(value) when those
guards pass — or better, delegate to the same send helper (e.g.,
sendMessage/sendHandler) used by the button so both paths share identical
validation logic.

In `@packages/ai-core/src/components/agent-chat/useAgentChatTransport.ts`:
- Around line 35-48: The parseUiMessages function accepts BodyInit (which can be
Blob/FormData/ArrayBuffer/etc.) but currently calls String(body) and JSON.parse,
producing "[object Blob]" for non-strings; update parseUiMessages to first check
typeof body === "string" before attempting JSON.parse, and if it's not a string
log a clear warning that includes the runtime type (use body?.constructor?.name
or typeof) and return []; keep the existing try/catch for JSON.parse for string
cases to preserve current error logging.

In `@packages/ai/src/skills/authoring/createSkillDraftStore.ts`:
- Around line 8-13: SkillDraft.author is currently required but the manifest
schema and tools (WriteManifestInput, patchManifest) treat it as optional,
causing undefined to be stored while the TS type claims it's string; change the
SkillDraft interface to make author optional (author?: string) and update
initialDraft to set author to undefined (or omit it) for consistency, and in
createSaveSkillTool ensure when building the draft passed to onSave you
normalize the value (use state.author ?? '' or similar) so callers never receive
an unexpectedly undefined string field; update references in
createSaveSkillTool, initialDraft, and any code constructing SkillDraft to match
the optional author type.

In `@packages/ai/src/skills/authoring/SkillDraftPreview.tsx`:
- Around line 27-44: Update the manifest-content detection to include the author
field: modify the hasManifestContent calculation (used to decide rendering of
ManifestCard vs placeholder) to also consider author (e.g., include author in
the Boolean/truthy check, optionally using .trim() if author is a string) so an
author-only draft renders ManifestCard; refer to hasManifestContent and
ManifestCard (props: name, description, author) when making the change.

In `@packages/ai/src/skills/manifest.ts`:
- Around line 27-38: The schema uses the old Zod v3 `{ message: '...' }` error
option (seen on id/version regex and name/description min calls); update all
custom error objects to Zod v4's `{ error: '...' }` form for id, version, name,
and description (e.g. the z.string().regex(KEBAB_CASE, {...}) and
z.string().min(1, {...}) calls) so validation messages follow Zod 4 conventions
and avoid the deprecated API.

---

Nitpick comments:
In `@examples/ai-agent/src/components/SkillsButton.tsx`:
- Around line 87-91: The collision-resolution loop using while(true) must be
bounded; replace it with a capped loop (e.g., define a MAX_ATTEMPTS constant and
iterate up to that many times while calling skillStorage.resolveSkillId to check
candidateId), incrementing counter and updating candidateId as now, and if
MAX_ATTEMPTS is exceeded throw a clear error (or reject) mentioning the baseId
and that unique ID allocation failed; update the code around candidateId,
baseId, counter and the call to skillStorage.resolveSkillId to use this bounded
approach.

In `@examples/ai-agent/src/store.ts`:
- Around line 27-49: Hoist the module-level skillStorage and cachedListings into
the room Zustand store by adding them to a slice (e.g., include skillStorage:
InMemorySkillStorage and cachedListings: SkillListing[] in the slice state and a
refreshSkillListings action), initialize skillStorage inside the slice
constructor, call refreshSkillListings once after creation, and subscribe to
skillStorage within the slice to update cachedListings via set(); then update
any callers (for example getInstructions) to read cachedListings from get()
instead of the closed-over module variable so lifecycle and teardown are managed
by the store.

In `@packages/ai-core/src/components/agent-chat/index.tsx`:
- Around line 139-150: Update the JSDoc for useMessagesObserver to accurately
state that the effect runs on initial mount and whenever the messages array
changes (not just "stream deltas"), so onMessagesChange will be invoked
immediately with the initial messages (initialMessages ?? []) and on every
subsequent messages array update; reference the function name
useMessagesObserver and its parameters (messages, onMessagesChange) in the
comment so consumers know this includes the initial render and can opt out if
they only want post-send updates.

In `@packages/ai-core/src/components/agent-chat/MessageView.tsx`:
- Line 8: The empty MARKDOWN_COMPONENTS constant in MessageView.tsx is unused
and misleading; remove the constant and its use by deleting MARKDOWN_COMPONENTS
and removing the components={MARKDOWN_COMPONENTS} prop from the ReactMarkdown
invocation (ReactMarkdown defaults are sufficient), or if you want to keep a
placeholder, inline {} directly as components={{}} on the ReactMarkdown call and
add a brief comment indicating it's intentional for future overrides; update the
ReactMarkdown usage accordingly.

In `@packages/ai/src/skills/storage.ts`:
- Around line 69-117: The SkillStorage interface lacks a subscribe/unsubscribe
API which InMemorySkillStorage currently exposes, causing consumers to depend on
the concrete InMemorySkillStorage instead of the abstraction; add a subscribe
method to the SkillStorage interface (e.g., subscribe(listener: () => void): ()
=> void) so callers can register a listener that is invoked after mutations
(writeSkill/deleteSkill) and receive an unsubscribe function, and then
implement/forward that behavior in InMemorySkillStorage (and any other backends)
so UI code can remain backend-agnostic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33479267-056d-4c90-a27f-cd4f1943b687

📥 Commits

Reviewing files that changed from the base of the PR and between 4ffaf6c and 8b6853a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (40)
  • examples/ai-agent/README.md
  • examples/ai-agent/package.json
  • examples/ai-agent/src/agents/WeatherAgent.ts
  • examples/ai-agent/src/components/MainView.tsx
  • examples/ai-agent/src/components/SkillsButton.tsx
  • examples/ai-agent/src/skills/InMemorySkillStorage.ts
  • examples/ai-agent/src/skills/getModel.ts
  • examples/ai-agent/src/skills/querySqlTool.ts
  • examples/ai-agent/src/skills/runSkillTool.ts
  • examples/ai-agent/src/skills/seedSkills.ts
  • examples/ai-agent/src/skills/skillPrompt.ts
  • examples/ai-agent/src/store.ts
  • packages/ai-core/README.md
  • packages/ai-core/src/components/FlatAgentRenderer.tsx
  • packages/ai-core/src/components/agent-chat/ChatInput.tsx
  • packages/ai-core/src/components/agent-chat/MessageView.tsx
  • packages/ai-core/src/components/agent-chat/SuggestionStrip.tsx
  • packages/ai-core/src/components/agent-chat/ToolActivityLine.tsx
  • packages/ai-core/src/components/agent-chat/index.tsx
  • packages/ai-core/src/components/agent-chat/toolCallAdapter.ts
  • packages/ai-core/src/components/agent-chat/useAgentChatTransport.ts
  • packages/ai-core/src/components/agent-chat/useStickyScroll.ts
  • packages/ai-core/src/index.ts
  • packages/ai/README.md
  • packages/ai/package.json
  • packages/ai/src/index.ts
  • packages/ai/src/skills/authoring/SkillAuthoringAgent.ts
  • packages/ai/src/skills/authoring/SkillAuthoringContext.ts
  • packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx
  • packages/ai/src/skills/authoring/SkillDraftPreview.tsx
  • packages/ai/src/skills/authoring/createSkillDraftStore.ts
  • packages/ai/src/skills/authoring/index.ts
  • packages/ai/src/skills/authoring/systemPrompt.ts
  • packages/ai/src/skills/authoring/tools.ts
  • packages/ai/src/skills/errors.ts
  • packages/ai/src/skills/index.ts
  • packages/ai/src/skills/manifest.ts
  • packages/ai/src/skills/storage.ts
  • packages/deck/package.json
  • packages/deck/tsconfig.json
💤 Files with no reviewable changes (1)
  • packages/deck/package.json

Comment thread examples/ai-agent/src/skills/InMemorySkillStorage.ts
Comment thread examples/ai-agent/src/store.ts
Comment thread packages/ai-core/README.md Outdated
Comment thread packages/ai-core/src/components/agent-chat/ChatInput.tsx Outdated
Comment thread packages/ai-core/src/hooks/useLocalAgentChatTransport.ts
Comment thread packages/ai/src/skills/authoring/createSkillDraftStore.ts
Comment thread packages/ai/src/skills/authoring/SkillDraftPreview.tsx Outdated
Comment thread packages/ai/src/skills/manifest.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/ai-agent/src/skills/InMemorySkillStorage.ts`:
- Around line 126-160: The writeSkill implementation in InMemorySkillStorage
silently overwrites an existing skill and doesn't verify the caller-provided id
matches the manifest id; update async writeSkill to first validate that
content.manifest.id (or content.manifest?.id) equals the id parameter (or set
manifest.id = id if intended) and then check rootMap.has(id) before writing—if a
skill already exists, throw a clear SkillManifestError (or a ConflictError) to
prevent silent overwrite; perform these checks prior to creating the files array
and only call rootMap.set(id, ...) and this.notify() after validation succeeds
to ensure no accidental data loss or id mismatch.
- Around line 76-78: The listRoots method currently returns the ROOTS array by
reference which allows callers to mutate stored SkillRoot objects; modify
InMemorySkillStorage.listRoots to return defensive copies by mapping over ROOTS
and returning shallow-cloned entries (e.g., create new objects for each
SkillRoot) so callers cannot alter the internal ROOTS state and writable flags
remain stable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11271d66-7243-4d0f-8297-0f25825558e4

📥 Commits

Reviewing files that changed from the base of the PR and between 8b6853a and 9ab7a5c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • examples/ai-agent/src/components/SkillsButton.tsx
  • examples/ai-agent/src/skills/InMemorySkillStorage.ts
  • examples/ai-agent/src/store.ts
  • packages/ai-core/README.md
  • packages/ai-core/src/components/agent-chat/ChatInput.tsx
  • packages/ai-core/src/components/agent-chat/MessageView.tsx
  • packages/ai-core/src/components/agent-chat/useAgentChatTransport.ts
  • packages/ai-core/src/index.ts
  • packages/ai/src/index.ts
  • packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx
  • packages/ai/src/skills/authoring/SkillDraftPreview.tsx
  • packages/ai/src/skills/authoring/createSkillDraftStore.ts
  • packages/ai/src/skills/authoring/index.ts
  • packages/ai/src/skills/authoring/tools.ts
  • packages/ai/src/skills/manifest.ts
  • packages/cells/src/components/VegaConfigPanel.tsx
✅ Files skipped from review due to trivial changes (2)
  • packages/cells/src/components/VegaConfigPanel.tsx
  • packages/ai-core/README.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/ai/src/skills/authoring/SkillDraftPreview.tsx
  • packages/ai/src/skills/authoring/createSkillDraftStore.ts
  • packages/ai-core/src/components/agent-chat/useAgentChatTransport.ts
  • packages/ai-core/src/index.ts
  • packages/ai-core/src/components/agent-chat/MessageView.tsx
  • packages/ai/src/skills/authoring/index.ts
  • packages/ai-core/src/components/agent-chat/ChatInput.tsx
  • packages/ai/src/index.ts
  • examples/ai-agent/src/store.ts
  • packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx
  • packages/ai/src/skills/manifest.ts

Comment thread examples/ai-agent/src/skills/InMemorySkillStorage.ts
Comment thread examples/ai-agent/src/skills/InMemorySkillStorage.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/ai/src/skills/composite.ts (1)

19-26: ⚡ Quick win

Consider parallelizing async aggregation for better I/O performance.

Both listRoots() and the bulk path of listSkills() currently aggregate child results sequentially. If children perform I/O, parallelizing with Promise.all would improve performance.

⚡ Proposed refactor to parallelize aggregation
 async listRoots(): Promise<SkillRoot[]> {
-  const out: SkillRoot[] = [];
-  for (const child of this.children) {
-    const roots = await child.listRoots();
-    out.push(...roots);
-  }
-  return out;
+  const allRoots = await Promise.all(
+    this.children.map((child) => child.listRoots()),
+  );
+  return allRoots.flat();
 }
 async listSkills(rootId?: string): Promise<SkillListing[]> {
   if (rootId !== undefined) {
     const child = await this.findChildForRoot(rootId);
     return child ? child.listSkills(rootId) : [];
   }
   // Duplicates (same id in multiple roots) are intentionally included so
   // callers can render conflict indicators.
-  const out: SkillListing[] = [];
-  for (const child of this.children) {
-    const listings = await child.listSkills();
-    out.push(...listings);
-  }
-  return out;
+  const allListings = await Promise.all(
+    this.children.map((child) => child.listSkills()),
+  );
+  return allListings.flat();
 }

Also applies to: 36-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/skills/composite.ts` around lines 19 - 26, Both listRoots()
and listSkills() are aggregating child results sequentially which hurts
I/O-bound performance; change each to kick off all child.listRoots() /
child.listSkills() calls concurrently (e.g., map children => promise and await
Promise.all(promises)), then flatten the array of arrays into the final
SkillRoot[]/Skill[] (use Array.prototype.flat or manual concat) so the children
property is queried in parallel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/ai/src/skills/composite.ts`:
- Around line 19-26: Both listRoots() and listSkills() are aggregating child
results sequentially which hurts I/O-bound performance; change each to kick off
all child.listRoots() / child.listSkills() calls concurrently (e.g., map
children => promise and await Promise.all(promises)), then flatten the array of
arrays into the final SkillRoot[]/Skill[] (use Array.prototype.flat or manual
concat) so the children property is queried in parallel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b066769d-f331-4b7d-8744-f2f883d87c39

📥 Commits

Reviewing files that changed from the base of the PR and between 97f9624 and 01f3375.

📒 Files selected for processing (5)
  • examples/ai-agent/src/store.ts
  • packages/ai/README.md
  • packages/ai/src/skills/composite.ts
  • packages/ai/src/skills/index.ts
  • packages/ai/src/skills/storage.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ai/src/skills/index.ts
  • packages/ai/src/skills/storage.ts
  • examples/ai-agent/src/store.ts

* (highest priority first). Children must own disjoint `rootId`s — the
* composite does not validate this; violating it makes dispatch ambiguous.
*/
export class CompositeSkillStorage implements SkillStorage {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] CompositeSkillStorage is documented as a public import but is not exported from @sqlrooms/ai. The README shows import {CompositeSkillStorage} from '@sqlrooms/ai', but packages/ai/src/index.ts (line 53) only re-exports the errors, manifest helpers, storage types, and authoring APIs from ./skills. Consumers following the docs will hit “no exported member”. Add CompositeSkillStorage to the package root exports.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx`:
- Around line 53-55: The docblock for SkillAuthoringPanel is stale: it says the
component does not render an explicit Save button, but the implementation
renders one when onSave is provided. Update the comment above the
SkillAuthoringPanel component to reflect actual behavior: note that when the
onSave prop is passed the panel renders an explicit Save button (which calls
onSave/saveSkill flow) and otherwise relies on the agent-driven save; reference
the onSave prop and saveSkill function and the footer status pill in the
description so integrators aren't misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c9ce8e9-31eb-41b3-91ba-c97b6426beca

📥 Commits

Reviewing files that changed from the base of the PR and between 01f3375 and 1d66ece.

📒 Files selected for processing (2)
  • examples/ai-agent/src/components/SkillsButton.tsx
  • packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx

Comment on lines +53 to +55
* explicit Save button — the agent calls `saveSkill` once the draft is
* complete, and the status pill in the footer communicates progress.
*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale save-behavior docblock.

Line 53 now contradicts the implementation: the panel does render an explicit Save button when onSave is provided. This comment will mislead integrators.

Suggested doc fix
- * explicit Save button — the agent calls `saveSkill` once the draft is
- * complete, and the status pill in the footer communicates progress.
+ * optional explicit Save button — when `onSave` is provided, the footer
+ * shows Save and updates the status pill as persistence progresses.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* explicit Save button — the agent calls `saveSkill` once the draft is
* complete, and the status pill in the footer communicates progress.
*
* optional explicit Save button — when `onSave` is provided, the footer
* shows Save and updates the status pill as persistence progresses.
*
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/skills/authoring/SkillAuthoringPanel.tsx` around lines 53 -
55, The docblock for SkillAuthoringPanel is stale: it says the component does
not render an explicit Save button, but the implementation renders one when
onSave is provided. Update the comment above the SkillAuthoringPanel component
to reflect actual behavior: note that when the onSave prop is passed the panel
renders an explicit Save button (which calls onSave/saveSkill flow) and
otherwise relies on the agent-driven save; reference the onSave prop and
saveSkill function and the footer status pill in the description so integrators
aren't misled.

@ilyabo
ilyabo merged commit c3d2a9e into main May 18, 2026
3 checks passed
@ilyabo
ilyabo deleted the mihajlo/skills-and-wizard branch May 18, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants