From 085f54099313306f6acc0e6d958c99e0ec6ff2b1 Mon Sep 17 00:00:00 2001 From: yoen-velt Date: Wed, 24 Jun 2026 12:02:49 -0400 Subject: [PATCH 1/3] v6.0.0-beta.1 --- .../logs/agent-2-planning-v6.0.0-beta.1.md | 88 +++++++++ .claude/logs/agent-7-qa-v6.0.0-beta.1.md | 37 ++++ api-reference/sdk/api/api-methods.mdx | 172 ++++++++++++++++++ api-reference/sdk/models/data-models.mdx | 7 +- docs.json | 7 + release-notes/version-6/sdk-changelog.mdx | 40 ++++ release-notes/version-6/upgrade-guide.mdx | 79 ++++++++ 7 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 .claude/logs/agent-2-planning-v6.0.0-beta.1.md create mode 100644 .claude/logs/agent-7-qa-v6.0.0-beta.1.md create mode 100644 release-notes/version-6/sdk-changelog.mdx create mode 100644 release-notes/version-6/upgrade-guide.mdx diff --git a/.claude/logs/agent-2-planning-v6.0.0-beta.1.md b/.claude/logs/agent-2-planning-v6.0.0-beta.1.md new file mode 100644 index 000000000..aa68c834a --- /dev/null +++ b/.claude/logs/agent-2-planning-v6.0.0-beta.1.md @@ -0,0 +1,88 @@ +# Release Update Plan for v6.0.0-beta.1 + +## Overview +- Release Type: Major (modular SDK; lazy feature chunks) +- Key Changes: New `featureAllowList` modular semantics; 19 `preload*()` facade methods; 16 `getXElement()` accessors now return non-blocking lazy facades; new `Features.CRDT='crdt'` enum value; Transcription folded into recorder chunk; 2 bug fixes. +- Breaking Changes: No hard API break — existing code compiles/runs as-is (lazy facades cast to public element types; `featureAllowList` default = preload all = pre-modular behavior). Behavior change only. Upgrade-guide narrative already authored by Agent-1. +- Scope: Pure SDK infrastructure. No new UI components, primitives, or wireframes. + +## Areas Requiring Updates + +### 1. Data Models — `api-reference/sdk/models/data-models.mdx` +File: `/Users/yoenzhang/Downloads/docs/api-reference/sdk/models/data-models.mdx` + +- UPDATE `#### Config` → `featureAllowList` row (line 4584). Do NOT add a duplicate row. Rewrite description to cover modular behavior: default `undefined` ⇒ preloads ALL lazy chunks (pre-modular behavior); passing it preloads ONLY the listed feature chunks (others deferred until `preloadX()`/`getXElement()`); allow-list auto-extends when an omitted feature's accessor/preload is called. +- ADD to `#### Features` enum (table at lines 4594–4607): one new row `CRDT | 'crdt' | No | CRDT feature (lazy chunk velt-crdt.js)` in correct alphabetical position (between `AUDIO_HUDDLE` and `COMMENT`, i.e. after `COMMENT`? — alphabetical by Property: CRDT sorts before COMMENT? No: "CRDT" < "COMMENT"? compare char 2: R vs O → O`), isPlanExpired$ 3670. +- Method entry format: `#### name()` + 1-line description + `- Params:` + `- Returns:` + `- React Hook:` (+ optional `- [Full Documentation →]`). + +Changes: +- ADD a new subsection `### Modular SDK / Chunk Preloading` immediately AFTER `### Feature Gating` (insert after line 3675, before `### Localization` at 3678). Rationale: feature-gating semantics live here (disableFeatures/featureAllowList); keeps modular material in one discoverable block. Contents: + - Intro note: featureAllowList modular behavior (default undefined ⇒ preload all chunks; passing it preloads only listed chunks; inert-tag/auto-upgrade — tags placed before chunk loads render as inert HTMLUnknownElement and silently no-op until the chunk lands then custom-elements lifecycle upgrades them in place; allow-list AUTO-EXTENSION via ensureFeatureEnabled when an omitted feature's accessor/preload is called; tag-only features note: userInvite/userRequest/videoPlayer have no getXElement accessor → must use featureAllowList or preloadX()). Link featureAllowList to `/api-reference/sdk/models/data-models#config`. + - The 19 `preload*()` entries, each: `#### preload()` + "Loads the lazy chunk and registers its custom elements on demand." + `- Params: void` + `- Returns: Promise` + `- React Hook: n/a`. Note idempotent + non-throwing (logs on failure, resolves) once in the intro, not per-entry, to avoid 19× repetition. The 19: preloadComment, preloadCursor, preloadPresence, preloadHuddle, preloadRecorder, preloadNotification, preloadReaction, preloadArrow, preloadTag, preloadRewriter, preloadSelection, preloadArea, preloadActivity, preloadViews, preloadUserInvite, preloadUserRequest, preloadVideoPlayer, preloadCrdt, preloadLiveStateSync. + - Priority: High. +- ADD lazy-facade behavior note to the `### Feature Utilities` accessor section (short addition, NOT a per-accessor rewrite — return types/method surfaces UNCHANGED). Place one explanatory note at the top of `### Feature Utilities` (after line 3583): getXElement() now returns a non-blocking lazy facade (cast to the real element type, so existing code is unaffected); void methods are queued and flushed once the chunk resolves; promise methods await chunk-load then delegate with original args; observable/on(...) methods return a per-call ReplaySubject(1)-backed proxy stream that bridges once the chunk loads; each accessor also calls ensureFeatureEnabled(...). Recommendation: short subsection note, not a new top-level section. + - Priority: Medium. +- DO NOT add new `####` headings for getViewsElement/getNotificationElement/getReactionElement/getTagElement/getHuddleElement/getAreaElement — they are named in the release note's accessor list but have no existing entries. FLAG for orchestrator: confirm whether these accessors should be newly documented or are intentionally undocumented (see Open Questions). Do not fabricate. + +### 3. Documentation (new feature docs) +- No new standalone feature doc page required. Modular/preload is an SDK-infrastructure concern documented in api-methods.mdx + data-models.mdx + the existing upgrade-guide. Priority: N/A. + +### 4a. UI Customization — Wireframes +- N/A. No new wireframes. (Pure SDK infrastructure.) + +### 4b. UI Customization — Primitives +- N/A. No new primitive components or props. (Pure SDK infrastructure.) + +### 5. Upgrade Guide — `release-notes/version-6/upgrade-guide.mdx` +File: `/Users/yoenzhang/Downloads/docs/release-notes/version-6/upgrade-guide.mdx` +- Authored by Agent-1. No new breaking-change content from this planning pass (no hard break; default behavior is backward-compatible). If Agent-1's narrative needs the featureAllowList opt-in modular semantics, that already belongs here — NOT in the changelog. Priority: Low (verify only). + +### Bug fixes (items 7, 8) +- Item 7 (video player ensures recorder chunk load to upgrade subtitle/transcription embeds): changelog-only. No reference-doc edit. +- Item 8 (`` per-annotationId subscription so active-state highlight appears reliably): changelog-only. No reference-doc edit. +- Recommendation: behavior fixes, no API surface change → no data-models/api-methods edits. + +## Implementation Sequence +1. data-models.mdx: update `featureAllowList` description; add `Features.CRDT` row (after COMMENT). Effort: Low. +2. api-methods.mdx: add `### Modular SDK / Chunk Preloading` section with intro + 19 preload entries. Effort: Medium. +3. api-methods.mdx: add lazy-facade behavior note atop `### Feature Utilities`. Effort: Low. +4. Verify upgrade-guide already carries modular opt-in narrative; no changelog breaking-change leakage. Effort: Low. + +## Counts to verify downstream (Agent-6/7) +- 19 lazy chunks = 19 preload methods. CONSISTENT (preload list = 19; chunk list = 19). +- 16 getXElement accessors (3 tag-only features have none). CONSISTENT (19 features − 3 tag-only = 16). +- 3 tag-only features: userInvite, userRequest, videoPlayer. CONSISTENT. +- 1 new enum value: `Features.CRDT='crdt'`. CONSISTENT (only explicit new enum value). + +## Terminology to keep EXACT (Agent-6/7) +`featureAllowList`, `preloadComment`…`preloadLiveStateSync` (19), `getCrdtElement`, `Features.CRDT`, `ensureFeatureEnabled`, `LazyXElement` (internal — do not surface), chunk filenames `velt-comment.js`/`velt-recorder.js`/`velt-crdt.js`/etc. + +## Open Questions for Orchestrator (do NOT auto-resolve) +- DISCREPANCY: release note's featureAllowList key for Live Selection is `selection` (velt-selection.js), but `#### Features` enum documents `LIVE_SELECTION='liveSelection'`. Do not mass-rename. Confirm canonical key with user. +- Missing-key audit vs `#### Features` enum. Enum currently contains (12): AREA, ARROW, AUDIO_HUDDLE, COMMENT, CURSOR, HUDDLE, LIVE_STATE_SYNC, PRESENCE, TAG, RECORDER, REWRITER, LIVE_SELECTION. Release-note keys NOT present in enum: `crdt` (adding), `notification`, `reaction`, `activity`, `views`, `userInvite`, `userRequest`, `videoPlayer`. Only `CRDT` is the explicit new enum value in the release note → add ONLY CRDT now; flag the others for the user to confirm before adding (release note's simplified key column ≠ enum naming). +- getViewsElement/getNotificationElement/getReactionElement/getTagElement/getHuddleElement/getAreaElement named in accessor list but undocumented today — confirm whether to add entries. + +## Quality Checklist +- [x] New type/enum value flagged for data-models.mdx (Features.CRDT) +- [x] All 19 new preload APIs scoped for api-methods.mdx (grouped section + format) +- [x] featureAllowList description update planned (no duplicate row) +- [x] Lazy-facade note planned for Feature Utilities (no per-accessor rewrite) +- [x] LazyXElement confirmed internal — not documented +- [x] Wireframes (Agent-4) and Primitives (Agent-5) confirmed N/A +- [x] Bug fixes (7, 8) = changelog-only +- [x] Breaking changes → upgrade-guide ONLY, never changelog +- [x] selection/liveSelection discrepancy + missing-key audit flagged, not auto-resolved +- [x] Counts confirmed internally consistent (19/16/3/1) +- [x] Log written to `.claude/logs/agent-2-planning-v6.0.0-beta.1.md` diff --git a/.claude/logs/agent-7-qa-v6.0.0-beta.1.md b/.claude/logs/agent-7-qa-v6.0.0-beta.1.md new file mode 100644 index 000000000..3e4b4114e --- /dev/null +++ b/.claude/logs/agent-7-qa-v6.0.0-beta.1.md @@ -0,0 +1,37 @@ +## QA Summary for v6.0.0-beta.1 (Modular SDK, frontend) + +### Counts: preload 19/19 | accessors 16 expected (12 documented) | tag-only 3 | featureAllowList row 1 | LazyXElement public 0 + +### Per-check results (evidence) + +1. **PASS** — Exactly 19 `#### preload*()` entries in api-methods.mdx (L3693–3805). Heading names diff against the required list = exact match, in the specified order. +2. **PASS** — `Features` enum: `CRDT='crdt'` row present (L4600); `LIVE_SELECTION='selection'` (L4608, NOT `'liveSelection'`); `LIVE_STATE_SYNC='liveStateSync'` unchanged (L4603). The only `liveSelection` in the diff is the removal (`-` line); zero occurrences remain in any changed/new file. +3. **PASS** — Unrelated `'crdt'` on `ActivityFeatureType` (data-models L3849) is untouched and NOT in the diff. Only new `'crdt'` is the `Features.CRDT` row. +4. **PASS** — `Config.featureAllowList` is a single row (count=1); description rewritten for modular behavior (set ⇒ only listed chunks preloaded; omit/default `undefined` ⇒ all preloaded in background; `preload*()`/`getXElement()` on an omitted feature auto-enables it). +5. **PASS** — `LazyXElement`/`LazyCommentElement` documented as public type: 0 in api-methods.mdx, 0 in data-models.mdx. Mentioned only as internal facade classes in changelog L30. +6. **PASS** — 19 chunks = 19 preload methods. Tag-only `userInvite`/`userRequest`/`videoPlayer` consistently described as having "no element accessor" in changelog (L20), upgrade-guide (L39), and api-methods intro (L3683). No `get{UserInvite,UserRequest,VideoPlayer}Element` accessor exists (no contradiction). +7. **PASS** — docs.json valid JSON (`python3 -m json.tool` ok). "Version 6.0.0" group is the immediate sibling above "Version 5.0.0" (idx 0 vs 1). Lists `release-notes/version-6/upgrade-guide` + `release-notes/version-6/sdk-changelog`; both files exist on disk. +8. **PASS** — ``. 8 separate bullets (New Features 2 / Improvements 4 / Bug Fixes 2); no merged items. Opens with the fully-backward-compatible note. No "Breaking Changes" section in changelog. All 3 distinct `[Learn more →]` targets resolve to real pages. +9. **PASS** — upgrade-guide has `## Overview` / `## Breaking Changes` (states "None — fully backward compatible") / `## How to Upgrade`. Covers featureAllowList, preload*(), and getXElement auto-extension (`ensureFeatureEnabled`). React tabs use `client.*`, Other Frameworks use `Velt.*` (correct per-context convention). +10. **PASS** — Case-exact: `featureAllowList`, `preload`, `getCrdtElement` (api-methods L3308), `Features.CRDT`, `ensureFeatureEnabled` all correct. Chunk filenames `velt-comment.js`…`velt-live-state-sync.js` all correct (19 unique). Zero drift variants (`featureAllowlist`, `getCRDTElement`, `Features.Crdt`, etc. = 0). +11. **PASS** — `git status --short` shows ONLY the 5 expected paths + untracked logs. No Node/Python/velt-node/velt-py/version-5/wireframe/primitive edits. + +### git status --short (final) +``` + M api-reference/sdk/api/api-methods.mdx + M api-reference/sdk/models/data-models.mdx + M docs.json +?? .claude/logs/agent-2-planning-v6.0.0-beta.1.md +?? release-notes/version-6/ +``` +`git diff --name-only`: api-methods.mdx, data-models.mdx, docs.json + +### Fix applied (1) +1. **release-notes/version-6/sdk-changelog.mdx L28** — Changelog cited a non-existent `NotificationSource` enum when reassuring readers the unrelated `'crdt'` value is unchanged. `grep` confirmed `NotificationSource` exists nowhere in the repo; the unrelated `'crdt'` actually lives on `ActivityFeatureType` (data-models L3829/L3849, which the bullet links to). + - Fix: `NotificationSource` → `ActivityFeatureType`. Makes the prose verifiable against the linked page. + +### Re-flagged pre-existing gaps (NOT auto-resolved per instructions) +- **7 un-backfilled `Features` enum keys** — enum has 13 rows but 19 chunk features exist. Missing keys: `notification`, `reaction`, `activity`, `views`, `userInvite`, `userRequest`, `videoPlayer`. Pre-existing; out of this release's scope. +- **6 undocumented `getXElement()` accessors** — of 16 expected accessors (19 chunks − 3 tag-only), 6 are undocumented: `getAreaElement`, `getHuddleElement`, `getNotificationElement`, `getReactionElement`, `getTagElement`, `getViewsElement`. (12 accessors documented, incl. unrelated `contact`/`suggestion`.) Pre-existing. + +### Issues found: 1 diff --git a/api-reference/sdk/api/api-methods.mdx b/api-reference/sdk/api/api-methods.mdx index 136392f8d..837972883 100644 --- a/api-reference/sdk/api/api-methods.mdx +++ b/api-reference/sdk/api/api-methods.mdx @@ -3581,6 +3581,9 @@ Reset the state of Velt Button components. ### Feature Utilities + +In the modular SDK every `getXElement()` accessor returns a non-blocking lazy facade that kicks off the feature chunk load and returns immediately, preserving the synchronous, never-null contract. Calls made before the chunk finishes are bridged: `void` methods are queued and flushed once the chunk loads; promise-returning methods await the chunk load then delegate with the original args; `on(...)`/observable methods return a `ReplaySubject(1)`-backed proxy stream that bridges once the chunk lands, so subscribing early still delivers events. Each accessor also auto-enables its feature (`ensureFeatureEnabled`). Return types and method surfaces are unchanged, so existing code compiles and runs as-is. + #### getPresenceElement() Get the Presence Element Object to access the raw presence data. - Params: `void` @@ -3644,6 +3647,48 @@ Get the Arrow Object. - React Hook: `n/a` {/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} +#### getAreaElement() +Get the Area Element Object. +- Params: `void` +- Returns: `AreaElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + +#### getHuddleElement() +Get the Huddle Element Object. +- Params: `void` +- Returns: `HuddleElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + +#### getNotificationElement() +Get the Notification Element Object. +- Params: `void` +- Returns: `NotificationElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + +#### getReactionElement() +Get the Reaction Element Object. +- Params: `void` +- Returns: `ReactionElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + +#### getTagElement() +Get the Tag Element Object. +- Params: `void` +- Returns: `TagElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + +#### getViewsElement() +Get the Views Element Object. +- Params: `void` +- Returns: `ViewsElement` +- React Hook: `n/a` +{/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} + #### getSuggestionElement() Get the singleton `SuggestionElement` instance. Repeat calls return the same object. - Params: `void` @@ -3675,6 +3720,133 @@ To check if plan is expired or not. {/* - [Full Documentation →](/api-reference/sdk/api/api-methods#client) */} +### Modular SDK / Chunk Preloading + +In the modular SDK each feature is compiled into a lazy chunk fetched on demand. Opt in with the [`Config.featureAllowList`](/api-reference/sdk/models/data-models#config) init option to preload only the listed feature chunks; omitting `featureAllowList` preserves pre-modular behavior, preloading all chunks in the background. Feature tags placed before their chunk loads render inert and silently upgrade in place once the chunk lands. The `preload*()` methods are idempotent and non-throwing (they log on failure and resolve). Calling `getXElement()` or `preloadX()` for a feature omitted from `featureAllowList` auto-enables it (`ensureFeatureEnabled`), so you are never locked out. The tag-only features `userInvite`, `userRequest`, and `videoPlayer` have no element accessor — load them via `featureAllowList` or the matching `preload*()`. + +```js +await client.preloadComment(); +client.getCommentElement().openCommentSidebar(); + +// Tag-only feature (no element accessor): +await client.preloadUserRequest(); +``` + +#### preloadComment() +Eagerly fetches the `comment` feature chunk (`velt-comment.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadCursor() +Eagerly fetches the `cursor` feature chunk (`velt-cursor.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadPresence() +Eagerly fetches the `presence` feature chunk (`velt-presence.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadHuddle() +Eagerly fetches the `huddle` feature chunk (`velt-huddle.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadRecorder() +Eagerly fetches the `recorder` feature chunk (`velt-recorder.js`) and resolves once it is loaded and its custom elements are registered (including the transcription/subtitle embed elements, which are folded into the recorder chunk). Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadNotification() +Eagerly fetches the `notification` feature chunk (`velt-notification.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadReaction() +Eagerly fetches the `reaction` feature chunk (`velt-reaction.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadArrow() +Eagerly fetches the `arrow` feature chunk (`velt-arrow.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadTag() +Eagerly fetches the `tag` feature chunk (`velt-tag.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadRewriter() +Eagerly fetches the `rewriter` feature chunk (`velt-rewriter.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadSelection() +Eagerly fetches the `selection` feature chunk (`velt-selection.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadArea() +Eagerly fetches the `area` feature chunk (`velt-area.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadActivity() +Eagerly fetches the `activity` feature chunk (`velt-activity-log.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadViews() +Eagerly fetches the `views` feature chunk (`velt-views.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadUserInvite() +Eagerly fetches the `userInvite` feature chunk (`velt-user-invite.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadUserRequest() +Eagerly fetches the `userRequest` feature chunk (`velt-user-request.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadVideoPlayer() +Eagerly fetches the `videoPlayer` feature chunk (`velt-video-player.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadCrdt() +Eagerly fetches the `crdt` feature chunk (`velt-crdt.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + +#### preloadLiveStateSync() +Eagerly fetches the `liveStateSync` feature chunk (`velt-live-state-sync.js`) and resolves once it is loaded and its custom elements are registered. Idempotent; never throws. +- Params: `void` +- Returns: `Promise` +- React Hook: `n/a` + + ### Localization #### setLanguage() To set the language. diff --git a/api-reference/sdk/models/data-models.mdx b/api-reference/sdk/models/data-models.mdx index cbee938a2..8b2ee0557 100644 --- a/api-reference/sdk/models/data-models.mdx +++ b/api-reference/sdk/models/data-models.mdx @@ -4581,12 +4581,16 @@ The `dataProvider` event emits different resolver event types depending on the d | Property | Type | Required | Description | | ----------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | | `urlAllowList` | string[] | No | Restricts Velt features to specific pages by specifying partial URL strings. | -| `featureAllowList` | FeatureType[] | No | Only allows the provided Velt features to run. | +| `featureAllowList` | FeatureType[] | No | Allow-list of Velt features permitted to run. In the modular SDK it also controls chunk preloading: when set, only the listed feature chunks are preloaded (others load on demand via `preload*()` / `getXElement()`); when omitted (default `undefined`), all lazy feature chunks are preloaded in the background, preserving pre-modular behavior. Calling `preload*()` or `getXElement()` for an omitted feature auto-enables it. | | `userPlanAllowList` | string[] | No | Restricts Velt features to specific user plans. | | `userIdAllowList` | string[] | No | Restricts Velt features to specific users. | | `usePrefersColorScheme` | boolean | No | If set to true, listens to changes on the `prefers-color-scheme` media query to set the global theme of Velt components. | | `globalStyles` | boolean | No | Controls whether Velt's global CSS styles are loaded. Default: true | +Valid modular feature keys for `featureAllowList` are: `'comment'`, `'cursor'`, `'presence'`, `'huddle'`, `'recorder'`, `'notification'`, `'reaction'`, `'arrow'`, `'tag'`, `'rewriter'`, `'selection'`, `'area'`, `'activity'`, `'views'`, `'userInvite'`, `'userRequest'`, `'videoPlayer'`, `'crdt'`, and `'liveStateSync'`. + +Use these strings when configuring modular chunk preloading. The `Features` enum below documents public enum values; `Features.LIVE_SELECTION` remains `'liveSelection'` for backward compatibility, while the v6 modular allow-list key for Live Selection is `'selection'`. + #### Features --- @@ -4597,6 +4601,7 @@ The `dataProvider` event emits different resolver event types depending on the d | `ARROW` | `'arrow'` | No | Arrow feature for drawing arrows | | `AUDIO_HUDDLE` | `'audioHuddle'` | No | Audio huddle feature for voice conversations | | `COMMENT` | `'comment'` | No | Comment feature for adding comments | +| `CRDT` | `'crdt'` | No | CRDT feature (lazy chunk `velt-crdt.js`) | | `CURSOR` | `'cursor'` | No | Cursor feature for showing user cursors | | `HUDDLE` | `'huddle'` | No | Huddle feature for video conversations | | `LIVE_STATE_SYNC` | `'liveStateSync'` | No | Live state sync feature | diff --git a/docs.json b/docs.json index cb26c29fe..de327eee5 100644 --- a/docs.json +++ b/docs.json @@ -1060,6 +1060,13 @@ { "group": "Release Notes", "pages": [ + { + "group": "Version 6.0.0", + "pages": [ + "release-notes/version-6/upgrade-guide", + "release-notes/version-6/sdk-changelog" + ] + }, { "group": "Version 5.0.0", "pages": [ diff --git a/release-notes/version-6/sdk-changelog.mdx b/release-notes/version-6/sdk-changelog.mdx new file mode 100644 index 000000000..82ed94d8f --- /dev/null +++ b/release-notes/version-6/sdk-changelog.mdx @@ -0,0 +1,40 @@ +--- +title: "Velt SDK Changelog" +rss: true +description: Release Notes of changes added to the core Velt SDK +--- + +### Libraries +- `@veltdev/react` +- `@veltdev/client` +- `@veltdev/sdk` + + + +First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backward compatible — existing integrations work unchanged. + +### New Features + +- [**Core**]: **Modular SDK** — each feature now compiles into its own on-demand chunk (19 chunks: comment, cursor, presence, huddle, recorder, notification, reaction, arrow, tag, rewriter, selection, area, activity, views, userInvite, userRequest, videoPlayer, crdt, liveStateSync), and the eager `velt.js` core holds only bootstrap + facade. Pass `featureAllowList: ['comment','presence']` at init to preload only the features you use; omitting it preserves pre-modular behavior (all chunks preloaded in the background). Tags placed before their chunk loads render inert and auto-upgrade in place once the chunk lands. [Learn more →](/release-notes/version-6/upgrade-guide) + +- [**Core**]: New `preload()` methods on the `Velt` / `Snippyly` facade — 19 idempotent, non-throwing preloaders (e.g. `preloadComment()`) that eagerly fetch a single feature chunk and resolve once it registers. Use them to warm a chunk ahead of use, or to load tag-only features (`userInvite`, `userRequest`, `videoPlayer`) that have no element accessor. [Learn more →](/api-reference/sdk/api/api-methods) + +### Improvements + +- [**Core**]: `getXElement()` accessors (e.g. `getCommentElement()`) now return non-blocking lazy facades that kick off the chunk load and return immediately, preserving the synchronous, never-null contract — void calls are queued and flushed, promise calls await the load, and `on(...)` / observable subscriptions bridge through once the chunk lands. Each accessor also auto-enables its feature. Existing code compiles and runs unchanged. [Learn more →](/api-reference/sdk/api/api-methods) + +- [**Recorder**]: Transcription is folded into the recorder chunk — loading `velt-recorder.js` also registers the subtitle/transcription `-embed` elements, avoiding a second round-trip. [Learn more →](/api-reference/sdk/api/api-methods) + +- [**Core**]: New `Features.CRDT = 'crdt'` enum value — CRDT is now its own lazy chunk, so `'crdt'` is a valid `featureAllowList` entry and `preloadCrdt()` / `getCrdtElement()` participate in feature gating. A separate, unrelated enum already uses a `'crdt'` value for a different purpose; that value is distinct and unchanged. [Learn more →](/api-reference/sdk/models/data-models) + +- [**Core**]: Live Selection now participates in modular loading under the `'selection'` feature key, matching `velt-selection.js` and `preloadSelection()`. Use `'selection'` in `featureAllowList` when you preload Live Selection explicitly. [Learn more →](/api-reference/sdk/models/data-models) + +- [**Core**]: New lazy element-facade classes (`LazyXElement`, e.g. `LazyCommentElement`) returned by `getXElement()` and cast to the public element types — no change to your code. [Learn more →](/api-reference/sdk/api/api-methods) + +### Bug Fixes + +- [**Recorder**]: The video player now loads transcription/subtitle embeds when their chunk is deferred — it proactively ensures the recorder chunk is loaded, upgrading the `` / `` elements so subtitles and transcription work even when the recorder chunk wasn't eagerly loaded. Fire-and-forget, idempotent, errors logged. [Learn more →](/api-reference/sdk/api/api-methods) + +- [**Reactions**]: The reaction pin active-state highlight is now reliable under the Angular Elements lifecycle. `` tracks its per-`annotationId` subscription separately and tears down only that subscription on `annotationId` change (not the blanket unsubscribe), so the user subscription survives and the current user's reaction pin reliably shows its active highlight (`active` class / `aria-pressed`). [Learn more →](/api-reference/sdk/api/api-methods) + + diff --git a/release-notes/version-6/upgrade-guide.mdx b/release-notes/version-6/upgrade-guide.mdx new file mode 100644 index 000000000..d5c9a1e6e --- /dev/null +++ b/release-notes/version-6/upgrade-guide.mdx @@ -0,0 +1,79 @@ +--- +title: Upgrade Guide +description: Upgrade to Velt SDK v6.0.0-beta.1 and opt into modular feature chunk loading. +--- + +## Overview +- **Modular SDK**: The SDK now ships as an eager `velt.js` core (bootstrap + facade) plus 19 lazy-loaded feature chunks (comment, cursor, presence, huddle, recorder, notification, reaction, and more). Each feature is fetched on demand instead of bundled into one eager payload. +- **Backward compatible**: The public `Velt` / `Snippyly` API is unchanged. Existing integrations work as-is. +- **New opt-in control APIs**: Use `featureAllowList` at init to preload only the features you use, and `preload()` to warm a chunk or load tag-only features ahead of time. + +## Breaking Changes + +**None — fully backward compatible.** Existing integrations work unchanged. If you do not pass `featureAllowList`, the SDK preserves pre-modular behavior and preloads all chunks in the background. + +## How to Upgrade + +Adopting the modular SDK is optional. Update the package and your existing code keeps working. To opt into on-demand loading, use the following control APIs. + +### 1. Preload only the features you use + +Pass `featureAllowList` at init to preload only the listed chunks. Omit it to keep the default (all chunks preloaded in the background). + +Valid modular feature keys are: `'comment'`, `'cursor'`, `'presence'`, `'huddle'`, `'recorder'`, `'notification'`, `'reaction'`, `'arrow'`, `'tag'`, `'rewriter'`, `'selection'`, `'area'`, `'activity'`, `'views'`, `'userInvite'`, `'userRequest'`, `'videoPlayer'`, `'crdt'`, and `'liveStateSync'`. + + + +```tsx + + {children} + +``` + + +```js +Velt.init(apiKey, { featureAllowList: ['comment', 'presence'] }); +``` + + + +### 2. Warm a chunk or load tag-only features with `preload()` + +Call a preloader to fetch a chunk ahead of use. This is also how you load tag-only features (`userInvite`, `userRequest`, `videoPlayer`) that have no element accessor. Preloaders are idempotent and non-throwing. + + + +```tsx +client.preloadComment(); +client.preloadUserInvite(); +``` + + +```js +Velt.preloadComment(); +Velt.preloadUserInvite(); +``` + + + +### 3. `getXElement()` accessors auto-load their chunk + +Every `getXElement()` accessor automatically loads its chunk and auto-extends the allow-list (`ensureFeatureEnabled`), so calling an accessor for a feature you didn't list still works — you're never locked out. + + + +```tsx +// Auto-loads the comment chunk and enables the feature, even if not in featureAllowList +const commentElement = client.getCommentElement(); +``` + + +```js +const commentElement = Velt.getCommentElement(); +``` + + + + +Tags placed before their chunk loads render inert and upgrade in place once the chunk lands — no remount or re-render required. + From c826014ebb79e397fc452128dd76261645a5ec57 Mon Sep 17 00:00:00 2001 From: yoen-velt Date: Wed, 24 Jun 2026 12:34:05 -0400 Subject: [PATCH 2/3] Cloud Functions June 23 --- ...a-v6.0.0-beta.1-memory-knowledge-search.md | 42 +++++ ...a.1-memory-knowledge-search-2026-06-23.mdx | 151 ++++++++++++++++++ ...ta.1-memory-knowledge-search-2026-06-23.md | 143 +++++++++++++++++ ai/memory/setup.mdx | 2 +- .../rest-apis/v2/memory/knowledge/search.mdx | 62 ++++++- api-reference/sdk/models/data-models.mdx | 39 ++++- backend-sdks/node.mdx | 10 +- release-notes/version-6/sdk-changelog.mdx | 8 +- 8 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 .claude/logs/agent-7-qa-v6.0.0-beta.1-memory-knowledge-search.md create mode 100644 .claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx create mode 100644 .claude/release-notes/plan-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.md diff --git a/.claude/logs/agent-7-qa-v6.0.0-beta.1-memory-knowledge-search.md b/.claude/logs/agent-7-qa-v6.0.0-beta.1-memory-knowledge-search.md new file mode 100644 index 000000000..dcca684e4 --- /dev/null +++ b/.claude/logs/agent-7-qa-v6.0.0-beta.1-memory-knowledge-search.md @@ -0,0 +1,42 @@ +## QA Summary for v6.0.0-beta.1 — Memory knowledge/search `includeRules` (backend REST slice) + +### Verdict: PASS — terminology consistent for this release; 0 edits required. + +> Note: a separate log `agent-7-qa-v6.0.0-beta.1.md` already exists covering the **frontend Modular SDK** slice of this same beta. This file covers the additive **Memory knowledge/search** slice; both are preserved. + +### Edits applied: 0 +Independent repo-wide re-verification confirmed Agent-6's alignment is clean. No casing/spelling drift of this release's terms exists in any published page, so no search/replace was performed. + +### Terminology sweep (published roots; `.claude/**` is in docs.json `ignore`, so unpublished) +| Term | Status | Notes | +| --- | --- | --- | +| `includeRules` (bool, default `false`) | OK | 12 occurrences, all correct case | +| `include_rules` / `includeRule` / `IncludeRules` | none | zero drift | +| `kind` = `"chunk"` / `"rule"` | OK | values quoted in search.mdx + changelog | +| `ruleId` | OK | no `ruleID` / `rule_id` anywhere | +| `category`, `recordsSearched` | OK | consistent | +| BANNED: custom_context, custom-context, knowledge_chunks, INV-022, (kind, id), getKnowledgeRules, KnowledgeSearchSchema, KnowledgeRuleHit | none in published pages | all confined to internal input artifact under `.claude/` (Mintlify-ignored) | + +### Link verification +- `/api-reference/rest-apis/v2/memory/knowledge/search` -> search.mdx EXISTS +- `/api-reference/rest-apis/v2/memory/knowledge/rules` -> rules.mdx EXISTS +- Changelog `[Learn more →]` targets resolve to real pages. +- Consistency: search.mdx says a rule hit's `ruleId` maps to the `id` field on List Extracted Rules; rules.mdx response uses `id`/`category`/`index` — matches. + +### MDX well-formedness (edited files) +- search.mdx: ParamField 7/7, Expandable 1/1, Note 1/1, ResponseExample 1/1; 16 fences (even). OK. +- sdk-changelog.mdx: Update 1/1. OK. +- ai/memory/setup.mdx: Note 3/3; 24 fences (even). OK. + +### Default contract confirmation +Chunk-only default still documented as unchanged: search.mdx L41 ("response shape is unchanged") and L103 ("byte-identical to the prior contract"). PASS. + +### Flags for human (no action taken — per constraints) +1. **Date discrepancy**: changelog `description="June 22, 2026"` vs Memory input artifact dated 2026-06-23. NOT changed per instruction. +2. **`limit` default**: search.mdx documents range 1–50 but no default value. Confirm if a default should be stated. +3. **docs.json nav**: `memory/knowledge/search` and `.../rules` are not listed in docs.json navigation (pre-existing). Not touched per constraint. +4. **Legitimate `metadata.apiKey`**: `self-host-data/field-inventory.mdx:461` pre-existing public-API-key row — left alone as instructed. +5. **Banned identifiers** live solely in `.claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx` (internal input, ignored by Mintlify). No leak. + +### Handoff +Documentation consistent for this release. Ready for Plugin Agent 1 (Skills Delta Extractor). diff --git a/.claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx b/.claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx new file mode 100644 index 000000000..c5f866fc5 --- /dev/null +++ b/.claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx @@ -0,0 +1,151 @@ +Velt SDK Release Notes - 2026-06-23 +Release Date: June 23, 2026 + +Summary +SDK Files Changed Insertions Deletions +shared-firebase-function 24 +1828 -38 + +Features +1. Knowledge Search: Opt-In Combined Chunk + Rule Search (includeRules) +What: The POST /v2/memory/knowledge/search endpoint now accepts an optional includeRules boolean field. When set to true, the search embeds the query exactly once and runs parallel vector searches over both the knowledge_chunks corpus (reference content) and the custom_context corpus (extracted rules). Both result sets are merged into a single relevance-ranked list, with each item tagged by a kind discriminator ("chunk" or "rule"). Rule hits additionally carry a ruleId (the rule's Firestore doc id) and an optional category field. + +Why: Callers previously had to query chunks and rules in separate requests and merge the results themselves. The combined search path embeds the query once and returns a unified, globally ranked list, reducing latency and Gemini embedding cost. + +Impact: Developers using /v2/memory/knowledge/search can now retrieve extracted rules alongside reference chunks in a single call. The existing response shape is fully preserved when includeRules is omitted or false — no migration required for existing integrations. + +Request: + +{ + "data": { + "query": "image format requirements", + "includeRules": true, + "limit": 5 + } +} + +Example: + +curl -X POST https://api.velt.dev/v2/memory/knowledge/search \ + -H "x-velt-api-key: $VELT_API_KEY" \ + -H "x-velt-auth-token: $VELT_AUTH_TOKEN" \ + -H "content-type: application/json" \ + -d '{ "data": { "query": "image format requirements", "includeRules": true, "limit": 5 } }' + +Request Fields (additions): + +Field Type Required Default Description +includeRules boolean no false true → also vector-search the custom_context (extracted-rules) corpus and merge results with chunk hits. false/omitted → chunk-only, response shape unchanged. Non-boolean is rejected (400). + +Updated limit semantics when includeRules: true: limit is the combined cap across both corpora (global top-N after merge), not a per-corpus quota. The existing 1–50 bounds still apply. + +Updated sourceId semantics when includeRules: true: sourceId narrows both corpora simultaneously. A single string restricts both searches to one source; a 2–30 element array fans out one query per source id in each corpus and merges the results. + +Response (when includeRules: true): + +{ + "result": { + "results": [ + { + "kind": "rule", + "ruleId": "rule_3f2...", + "sourceId": "src_checklist", + "text": "All images must be WEBP, max width 1200px", + "score": 0.09, + "category": "§Navigation Bar" + }, + { + "kind": "chunk", + "sourceId": "src_brief", + "text": "Our target audience is enterprise buyers...", + "score": 0.21 + } + ], + "recordsSearched": 2 + } +} + +Response shape rules: + +When includeRules is omitted or false, items have the existing shape { sourceId?, text, score? } with no kind, ruleId, or category keys — byte-identical to the prior response contract. +When includeRules: true, every item gains a kind field. kind: "chunk" items carry no additional fields beyond the base shape. kind: "rule" items also carry ruleId and an optional category. +Items are sorted ascending by cosine distance (score). Items from the recency fallback path (when embedding fails) have no score and sort last. +recordsSearched equals the number of items in the returned results array after merging and truncating to limit. + +Type: + +interface KnowledgeSearchResult { + sourceId?: string; + text: string; + score?: number; + // Present only when includeRules: true + kind?: 'chunk' | 'rule'; + ruleId?: string; // rule hits only + category?: string; // rule hits only, when set on the rule doc +} + +interface KnowledgeSearchResponse { + results: KnowledgeSearchResult[]; + recordsSearched: number; +} + +Merge contract (INV-022): The query is embedded exactly once per request and the resulting vector is shared across both corpus searches (no second Gemini embedding call). Both result sets are concatenated, sorted ascending by cosine distance, deduplicated by (kind, id) tuple (a chunk and a rule that happen to share the same id string are kept as two distinct items), then truncated to limit. + +Degraded-mode behavior: If the embedding call fails or times out, both corpora route to their recency fallback independently, returning the most-recently-ingested items for the workspace (honoring sourceId if provided). The endpoint never throws on embedding failure. + +Tenant safety: All rule-corpus queries carry the co-located metadata.apiKey tenant clause before the findNearest vector search, identical to the chunk-corpus path. sourceId is an additive filter on top of the tenant clause. + +API Changes +1. KnowledgeRuleHit — New Type for Rule Search Results +What: A new exported TypeScript interface KnowledgeRuleHit describes a single extracted-rule search hit returned by the internal getKnowledgeRules helper and surfaced in the knowledgeSearch response when includeRules: true. + +Why: The rule corpus (custom_context) stores rules with a different shape than knowledge chunks — keyed by a ruleId (the Firestore doc id) and carrying an optional rule category. KnowledgeRuleHit formalizes this shape for internal consumers. + +Impact: Internal SDK consumers calling getKnowledgeRules directly can type their results against this interface. + +Type: + +export interface KnowledgeRuleHit { + ruleId: string; + sourceId?: string; + text: string; + score?: number; // cosine distance; absent on recency-fallback hits + category?: string; // absent when the rule doc has no category +} + +Used in: knowledge-rules-search.ts → getKnowledgeRules(), MemoryService.knowledgeSearch() (merged result mapping) + +2. KnowledgeSearchSchema — New includeRules Field +What: KnowledgeSearchSchema (Zod) gains an optional includeRules: z.boolean().optional() field. The controller defaults an absent value to false before passing it to MemoryService.knowledgeSearch, preserving the existing chunk-only response shape. + +Why: Strict schema validation ensures that non-boolean values (e.g., the string "true" or the integer 1) are rejected with a 400 before reaching the search logic. + +Impact: Non-boolean values for includeRules now return a 400 validation error instead of being silently ignored. + +3. MemoryService.knowledgeSearch — Updated Signature +What: MemoryService.knowledgeSearch now accepts an optional includeRules?: boolean parameter and returns an updated result type that includes the optional kind, ruleId, and category fields when includeRules: true. + +Why: Required to orchestrate the combined chunk + rule search path while keeping the default chunk-only path unchanged. + +Impact: Existing callers that do not pass includeRules observe no change in behavior or response shape. + +Updated signature: + +static async knowledgeSearch(params: { + apiKey: string; + query: string; + sourceId?: string | string[]; + limit?: number; + includeRules?: boolean; +}): Promise<{ + results: Array<{ + sourceId?: string; + text: string; + score?: number; + kind?: 'chunk' | 'rule'; + ruleId?: string; + category?: string; + }>; + recordsSearched: number; +}> + +This should be appended to the v6.0.0-beta.1 release note. diff --git a/.claude/release-notes/plan-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.md b/.claude/release-notes/plan-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.md new file mode 100644 index 000000000..27700c9f4 --- /dev/null +++ b/.claude/release-notes/plan-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.md @@ -0,0 +1,143 @@ +# Release Update Plan for v6.0.0-beta.1 — Memory Knowledge Search (`includeRules`) + +## Overview +- Release Type: Minor (additive, backend-only REST feature; folded into v6.0.0-beta.1 changelog). +- Key Changes: `POST /v2/memory/knowledge/search` gains optional `includeRules` boolean. `true` → combined chunk + extracted-rule vector search, merged into one ranked list; each item tagged `kind` (`"chunk"`|`"rule"`); rule hits add `ruleId` and optional `category`. New exported TS type `KnowledgeRuleHit`. +- Breaking Changes: None. With `includeRules` omitted/false the response is byte-identical to the prior contract. No migration required. Do NOT add anything to any upgrade guide. +- SDK: `shared-firebase-function` (Velt Memory REST API). No frontend Velt SDK surface, no UI components. + +## Source of truth +- Raw note: `/Users/yoenzhang/Downloads/docs/.claude/release-notes/input-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.mdx` +- Changelog (already updated by Agent-1, 3 bullets): `/Users/yoenzhang/Downloads/docs/release-notes/version-6/sdk-changelog.mdx` lines 22, 24, 26. + +## Areas Requiring Updates + +### 1. REST Endpoint Page — PRIMARY (and only required doc edit) +- File: `/Users/yoenzhang/Downloads/docs/api-reference/rest-apis/v2/memory/knowledge/search.mdx` +- Priority: High +- This page documents types INLINE on the endpoint page (no `` components are used here; response shape is conveyed via prose + JSON code blocks). Agent-3 MUST match the existing conventions below, not introduce new component styles. + +**Existing conventions to preserve (do not change):** +- Frontmatter: `title: "Search Knowledge Base"`, `api: "POST https://api.velt.dev/v2/memory/knowledge/search"`. +- Section order: intro prose → `# Endpoint` → `# Headers` (two ``) → `# Body` → `#### Params` → `# Response` → `#### Success Response` → `#### Failure Response` → ``. +- Body params live inside a single `` wrapping an `` with nested `` children. +- Example requests are bare ` ```JSON ` blocks under `## **Example Requests**` with `#### ` sub-headings. +- Response shape is described in prose, then shown as ` ```JSON ` (Success/Failure) and duplicated in `` ` ```js `. + +**Exact edits for Agent-3:** + +a) Add a new body param `includeRules` inside the `` properties (place it after `limit`, before the ``): +``` + + When `true`, also vector-searches the extracted-rules corpus and merges those hits with the knowledge-chunk hits into one relevance-ranked list. When omitted or `false`, only knowledge chunks are searched and the response shape is unchanged. Must be a boolean; non-boolean values (e.g. the string `"true"` or `1`) are rejected with a `400`. + +``` + +b) Update the `limit` param description to add combined-cap semantics (keep it short): +- Current: "Maximum number of results to return." +- New: "Maximum number of results to return (1–50). When `includeRules` is `true`, this is the combined cap across both corpora (global top-N after the merge), not a per-corpus quota." +- NOTE (flag for human): the current `limit` description does not state the 1–50 bound; the raw note and the sibling `search.mdx` (judgments) both state 1–50. Adding "(1–50)" is consistent with the note; confirm there is no separate default value to document (sibling judgments `search` defaults to 10 — the note does not state a default for knowledge/search, so do NOT invent one). + +c) Update the `sourceId` param description to add the both-corpora behavior. Append one sentence: +- Append: "When `includeRules` is `true`, `sourceId` narrows both corpora simultaneously (single id restricts both searches; an array fans out one query per id per corpus, then merges)." + +d) Add a combined-search example under `## **Example Requests**` (new `#### Search chunks and extracted rules together` block): +```JSON +{ + "data": { + "query": "image format requirements", + "includeRules": true, + "limit": 5 + } +} +``` + +e) Update the `# Response` prose + add a combined-response code block. Keep the existing chunk-only Success Response as the default shape. Add after it a note + example for `includeRules: true`: +- Prose to add: "When `includeRules` is `true`, every item gains a `kind` field (`\"chunk\"` or `\"rule\"`). `kind: \"rule\"` items also include a `ruleId` and an optional `category`. `kind: \"chunk\"` items carry no extra fields. Items are sorted ascending by `score` (cosine distance, lower = more relevant); on embedding failure both corpora fall back to most-recently-ingested items, which have no `score` and sort last. `recordsSearched` equals the number of items returned after merge + truncation to `limit`." +- Combined-response JSON to add (label e.g. `#### Success Response (includeRules: true)`): +```JSON +{ + "result": { + "results": [ + { + "kind": "rule", + "ruleId": "rule_3f2...", + "sourceId": "src_checklist", + "text": "All images must be WEBP, max width 1200px", + "score": 0.09, + "category": "§Navigation Bar" + }, + { + "kind": "chunk", + "sourceId": "src_brief", + "text": "Our target audience is enterprise buyers...", + "score": 0.21 + } + ], + "recordsSearched": 2 + } +} +``` + +f) Cross-reference to the rules endpoint: add to the intro or response prose a link to `[List Extracted Rules](/api-reference/rest-apis/v2/memory/knowledge/rules)` so readers know where the rule corpus comes from (mirrors how `search.mdx` cross-links to `knowledge/search`). The `ruleId` returned here corresponds to the `id` field on the rules endpoint — state this relationship in one clause. + +**Decisions / omissions for the endpoint page:** +- Tenant safety (`metadata.apiKey` clause before `findNearest`): OMIT from public docs. It is an internal implementation invariant, not caller-facing. The existing page already covers the caller-facing tenant guarantee ("An unknown or cross-workspace `sourceId` returns empty results … never a cross-tenant read") in the `` — no change needed there. +- Merge contract INV-022 (embed once, concat, sort, dedup by `(kind, id)`, truncate): do NOT document the invariant id or internal steps. The caller-facing outcomes (single embed/lower latency, sorted-by-score, truncated-to-limit) are covered by the prose in (e). The dedup `(kind, id)` rule is internal — omit. +- Degraded-mode/recency: document ONLY the caller-facing outcome (no-score items sort last on embedding failure), already folded into (e). Do not describe the failure mechanism. + +### 2. Types / Data Models — document INLINE on the endpoint page; do NOT touch frontend `data-models.mdx` +- Decision: per project memory (`project_backend_sdk_type_doc_conventions.md`), backend/REST types are documented on their endpoint/Node pages, NOT in the frontend SDK `data-models.mdx`. There is also no REST-specific data-models page for memory (the memory dir has no types/models page; types are conveyed inline as JSON shapes on each endpoint). +- `KnowledgeSearchResult` / `KnowledgeSearchResponse`: already represented inline as the JSON response shape on `search.mdx`. The new optional `kind`/`ruleId`/`category` fields are covered by edit (e) above. No separate type page needed. +- `KnowledgeRuleHit`: this is an INTERNAL exported TS interface for consumers calling the internal `getKnowledgeRules` helper. It is not part of the public REST contract surfaced to API callers. Recommendation: do NOT add `KnowledgeRuleHit` to any public docs page — its fields (`ruleId`, `sourceId?`, `text`, `score?`, `category?`) are already reflected in the rule-hit JSON shape on `search.mdx`. Flag for human: confirm there is no Node SDK reference page for Memory that documents internal helpers (none found under `api-reference/`); if one is later added, that is where `KnowledgeRuleHit` would belong — not `data-models.mdx`. +- Priority: N/A (no new type page). + +### 3. Conceptual Docs — `ai/memory/setup.mdx` cross-reference (OPTIONAL, Low) +- File: `/Users/yoenzhang/Downloads/docs/ai/memory/setup.mdx` +- Context: lines 104–114 contain a "Search inside your knowledge base" block with a `/knowledge/search` curl example and a sentence about `sourceId`/`score`. It does not mention `includeRules`. +- Recommended (Low priority, optional): append one sentence after line 114 noting the combined search, e.g.: "Pass `includeRules: true` to also return extracted rules alongside chunks in a single ranked list — see [Search Knowledge Base](/api-reference/rest-apis/v2/memory/knowledge/search)." Do NOT add a full second curl example here; keep setup concise. The page already links to Search Knowledge Base, so this is an enhancement, not a correctness fix. +- Flag for human: decide whether `setup.mdx` should advertise `includeRules` at all, or stay minimal and defer entirely to the endpoint page. Default recommendation: add the single sentence; skip if the human prefers setup stay lean. +- `ai/memory/overview.mdx`: checked — no `knowledge/search` reference; no change. + +### 4. docs.json Navigation — NO CHANGE (verify only) +- File: `/Users/yoenzhang/Downloads/docs/docs.json` +- Finding: the string `memory` does NOT appear anywhere in `docs.json`. The entire `api-reference/rest-apis/v2/memory/**` tree (including `knowledge/search`) is NOT registered in the published navigation. These pages are reachable only via direct in-page links (e.g. the changelog `[Learn more →]` links and the cross-refs in `setup.mdx`/`search.mdx`), which resolve by file path regardless of nav membership. +- Action: none. We are editing an existing page, not adding one, so no nav entry is required. Do NOT add the memory pages to nav as part of this release (out of scope; would be a separate editorial decision). +- Flag for human: the memory REST endpoints being absent from nav is pre-existing and may be intentional (beta/unlisted). Surface it but do not fix it here. + +### 4a. UI Customization — Wireframes — NO-OP +- Agent-4: nothing to do. Backend-only REST feature; no Velt UI components, no wireframes. Confirmed no component/primitive names in the release note. + +### 4b. UI Customization — Primitives — NO-OP +- Agent-5: nothing to do. No new primitive components or props. No `Velt*` components involved. + +### 5. Upgrade Guide — NO-OP +- No breaking changes. Nothing for any `upgrade-guide.mdx`. Breaking-change rule N/A. + +## Implementation Sequence +1. Agent-3: edit `knowledge/search.mdx` per section 1 (a–f). Single file, ~Medium effort. This is the only required change. +2. Agent-3 (optional, Low): add the one-sentence cross-ref in `ai/memory/setup.mdx` per section 3, if human approves. +3. Agent-4: no-op (wireframes). Skip. +4. Agent-5: no-op (primitives). Skip. +5. Agent-6 (alignment): scan repo for consistent naming of `includeRules`, `kind`, `ruleId`, `category`, "extracted rules", "knowledge chunks" / "knowledge base", "recordsSearched". Ensure `kind` values are quoted as `"chunk"`/`"rule"`. Verify the new `search.mdx` content matches the 3 changelog bullets (lines 22/24/26) and the raw note. Low effort. +6. Agent-7 (QA terminology): verify no stray variants (e.g. `include_rules`, `includeRule`, `ruleID`, `rule_id`, "custom_context" leaking into public prose — it should NOT appear; public docs say "extracted rules"). Confirm all `[Learn more →]` / cross-ref links resolve to `/api-reference/rest-apis/v2/memory/knowledge/search` and `/api-reference/rest-apis/v2/memory/knowledge/rules`. No repo-wide rename needed (no terminology changed; this is purely additive). + +## Quality Checklist +- [ ] `includeRules` body param added to `search.mdx` (type boolean, default false, 400-on-non-boolean noted). +- [ ] `limit` updated to combined-cap-when-true semantics (and 1–50 confirmed with human re: default). +- [ ] `sourceId` updated to both-corpora-when-true semantics. +- [ ] Combined request example added; combined response example with `kind`/`ruleId`/`category` added. +- [ ] Response prose explains `kind`, rule-only `ruleId`/`category`, score sort + no-score recency items last, `recordsSearched`. +- [ ] Cross-link to `knowledge/rules` added; `ruleId` ↔ rules `id` relationship stated. +- [ ] Tenant-safety / INV-022 / dedup internals OMITTED from public docs (confirmed). +- [ ] No edits to frontend `data-models.mdx`; `KnowledgeRuleHit` kept out of public docs (internal type). +- [ ] No `upgrade-guide` edits (no breaking changes). +- [ ] No `docs.json` nav changes (memory tree not in nav; verified). +- [ ] Agent-4 / Agent-5 confirmed no-op. +- [ ] Existing `` / `` / `` conventions matched; no new component styles introduced. +- [ ] Plan written to `/Users/yoenzhang/Downloads/docs/.claude/release-notes/plan-v6.0.0-beta.1-memory-knowledge-search-2026-06-23.md`. + +## Open questions flagged for the human +1. `limit` default for knowledge/search: not stated in the note (judgments `search` defaults to 10). Do not invent one — confirm if a default should be documented. +2. `setup.mdx`: add the optional one-sentence `includeRules` cross-ref, or leave setup lean? +3. Memory REST endpoints are entirely absent from `docs.json` nav (pre-existing). Intentional (unlisted/beta), or should they be added separately (out of scope for this release)? diff --git a/ai/memory/setup.mdx b/ai/memory/setup.mdx index e8de62cad..edeb5c03e 100644 --- a/ai/memory/setup.mdx +++ b/ai/memory/setup.mdx @@ -111,7 +111,7 @@ curl -X POST https://api.velt.dev/v2/memory/knowledge/search \ # -> { "result": { "results": [ { "sourceId": "src_9a8...", "text": "Always cite a peer-reviewed source...", "score": 0.18 } ], "recordsSearched": 1 } } ``` -`sourceId` is optional and may be a single id or an array of 2 to 30 ids (fanned out then merged); `score` is the cosine distance (lower = more relevant). This endpoint is workspace-scoped (no `organizationId`/`documentId`). Full request shapes: [Ingest Knowledge](/api-reference/rest-apis/v2/memory/knowledge/ingest), [Get Ingest Status](/api-reference/rest-apis/v2/memory/knowledge/ingest-status), [List Extracted Rules](/api-reference/rest-apis/v2/memory/knowledge/rules), [List Knowledge Sources](/api-reference/rest-apis/v2/memory/knowledge/list), and [Search Knowledge Base](/api-reference/rest-apis/v2/memory/knowledge/search). +`sourceId` is optional and may be a single id or an array of 2 to 30 ids (fanned out then merged); `score` is the cosine distance (lower = more relevant). This endpoint is workspace-scoped (no `organizationId`/`documentId`). Pass `includeRules: true` to also return extracted rules alongside chunks in a single ranked list — see [Search Knowledge Base](/api-reference/rest-apis/v2/memory/knowledge/search). Full request shapes: [Ingest Knowledge](/api-reference/rest-apis/v2/memory/knowledge/ingest), [Get Ingest Status](/api-reference/rest-apis/v2/memory/knowledge/ingest-status), [List Extracted Rules](/api-reference/rest-apis/v2/memory/knowledge/rules), [List Knowledge Sources](/api-reference/rest-apis/v2/memory/knowledge/list), and [Search Knowledge Base](/api-reference/rest-apis/v2/memory/knowledge/search). **Large or binary files (up to 30 MB):** call [Get Upload URL](/api-reference/rest-apis/v2/memory/knowledge/upload-url) with `{ mimeType, fileSize, fileName }`, PUT the raw bytes to the returned `uploadUrl`, then call `ingest` with `{ "source": "fileRef", "fileRef": "", "mimeType": "..." }`. diff --git a/api-reference/rest-apis/v2/memory/knowledge/search.mdx b/api-reference/rest-apis/v2/memory/knowledge/search.mdx index ff662074c..80463a79f 100644 --- a/api-reference/rest-apis/v2/memory/knowledge/search.mdx +++ b/api-reference/rest-apis/v2/memory/knowledge/search.mdx @@ -30,15 +30,19 @@ Use this API to run a semantic vector search over the **content of your ingested - Restrict the search to one or more knowledge sources. A single id restricts to that source; an array of **2 to 30** ids fans out one prefiltered nearest-neighbour search per id and merges/re-ranks the combined results by distance. Omit to search the entire workspace knowledge base. + Restrict the search to one or more knowledge sources. A single id restricts to that source; an array of **2 to 30** ids fans out one prefiltered nearest-neighbour search per id and merges/re-ranks the combined results by distance. Omit to search the entire workspace knowledge base. When `includeRules` is `true`, `sourceId` narrows both corpora simultaneously (a single id restricts both searches; an array fans out one query per id per corpus, then merges). - Maximum number of results to return. + Maximum number of results to return (1–50). When `includeRules` is `true`, this is the combined cap across both corpora (global top-N after the merge), not a per-corpus quota. + + + + When `true`, also vector-searches the extracted-rules corpus and merges those hits with the knowledge-chunk hits into one relevance-ranked list. When omitted or `false`, only knowledge chunks are searched and the response shape is unchanged. Must be a boolean; non-boolean values (e.g. the string `"true"` or `1`) are rejected with a `400`. - This endpoint is **workspace-scoped**. `organizationId` and `documentId` are not accepted; the strict schema rejects them rather than silently ignoring them. An unknown or cross-workspace `sourceId` returns empty results (never an error or a cross-tenant read). On embedding failure, the endpoint falls back to a recency query. + This endpoint is **workspace-scoped**. `organizationId` and `documentId` are not accepted; the strict schema rejects them rather than silently ignoring them. An unknown or cross-workspace `sourceId` returns empty results (never an error or a cross-tenant read). On embedding failure the endpoint never throws; it falls back to a recency query, returning the most-recently-ingested items (honoring `sourceId`). When `includeRules` is `true`, both corpora fall back to recency independently. @@ -80,9 +84,31 @@ Use this API to run a semantic vector search over the **content of your ingested } ``` +#### Search chunks and extracted rules together + +```JSON +{ + "data": { + "query": "image format requirements", + "includeRules": true, + "limit": 5 + } +} +``` + +```bash cURL +curl -X POST https://api.velt.dev/v2/memory/knowledge/search \ + -H "x-velt-api-key: $VELT_API_KEY" \ + -H "x-velt-auth-token: $VELT_AUTH_TOKEN" \ + -H "content-type: application/json" \ + -d '{ "data": { "query": "image format requirements", "includeRules": true, "limit": 5 } }' +``` + # Response -Each result includes the matched `sourceId`, the chunk `text`, and a `score`: the cosine distance, where **lower is more relevant**. `recordsSearched` reports how many sources were queried. +Each result includes the matched `sourceId`, the result `text`, and, when vector search succeeds, a `score`: the cosine distance, where **lower is more relevant**. For the default chunk-only response, `recordsSearched` remains unchanged from the prior contract; when `includeRules` is `true`, it equals the number of returned items after merging and truncating to `limit`. + +When `includeRules` is omitted or `false`, the response is byte-identical to the prior contract: items have the shape `{ sourceId?, text, score? }` with no `kind`, `ruleId`, or `category` keys. #### Success Response @@ -101,6 +127,34 @@ Each result includes the matched `sourceId`, the chunk `text`, and a `score`: th } ``` +When `includeRules` is `true`, every item gains a `kind` field (`"chunk"` or `"rule"`). `kind: "rule"` items also include a `ruleId` and an optional `category`; `kind: "chunk"` items carry no extra fields. Items are sorted ascending by `score` (cosine distance, lower is more relevant); recency-fallback items (returned when embedding fails) have no `score` and sort last. The endpoint embeds the query once, reuses that vector for both corpora, concatenates both result sets, deduplicates by the `(kind, id)` tuple, and truncates the merged list to `limit`. Here, `recordsSearched` equals the number of items returned after truncation. A rule hit's `ruleId` corresponds to the `id` field on [List Extracted Rules](/api-reference/rest-apis/v2/memory/knowledge/rules). + +#### Success Response (includeRules: true) + +```JSON +{ + "result": { + "results": [ + { + "kind": "rule", + "ruleId": "rule_3f2...", + "sourceId": "src_checklist", + "text": "All images must be WEBP, max width 1200px", + "score": 0.09, + "category": "§Navigation Bar" + }, + { + "kind": "chunk", + "sourceId": "src_brief", + "text": "Our target audience is enterprise buyers...", + "score": 0.21 + } + ], + "recordsSearched": 2 + } +} +``` + #### Failure Response ```JSON diff --git a/api-reference/sdk/models/data-models.mdx b/api-reference/sdk/models/data-models.mdx index 8b2ee0557..ec32bc193 100644 --- a/api-reference/sdk/models/data-models.mdx +++ b/api-reference/sdk/models/data-models.mdx @@ -11406,8 +11406,43 @@ Request payload for `sdk.api.memory.searchKnowledge`. | Field | Type | Required | Description | |-------|------|----------|-------------| | `query` | `string` | Yes | Search query. | -| `sourceId` | `string \| string[]` | No | Omit for whole workspace; array of 2..30 ids. | -| `limit` | `number` | No | Result limit, 1..50, default 10. | +| `sourceId` | `string \| string[]` | No | Omit for whole workspace; array of 2..30 ids. When `includeRules` is `true`, narrows both knowledge chunks and extracted rules. | +| `limit` | `number` | No | Result limit, 1..50, default 10. When `includeRules` is `true`, this is the combined cap after merging both corpora. | +| `includeRules` | `boolean` | No | Defaults to `false`. When `true`, also searches extracted rules and merges them with knowledge-chunk hits. Non-boolean values are rejected. | + +### `MemoryKnowledgeSearchResult` (Node) + +Result item returned by `sdk.api.memory.searchKnowledge`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `sourceId` | `string` | No | Knowledge source that produced the hit. | +| `text` | `string` | Yes | Matched chunk text or extracted-rule text. | +| `score` | `number` | No | Cosine distance; lower is more relevant. Absent for recency-fallback hits. | +| `kind` | `'chunk' \| 'rule'` | No | Present only when `includeRules` is `true`; distinguishes knowledge chunks from extracted rules. | +| `ruleId` | `string` | No | Rule document id. Present only on `kind: 'rule'` hits. | +| `category` | `string` | No | Optional rule category. Present only on rule hits when the rule has a category. | + +### `MemoryKnowledgeSearchResponse` (Node) + +Response payload returned under `result` from `sdk.api.memory.searchKnowledge`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `results` | `MemoryKnowledgeSearchResult[]` | Yes | Search results. When `includeRules` is omitted or `false`, result items keep the existing chunk-only shape with no `kind`, `ruleId`, or `category`. | +| `recordsSearched` | `number` | Yes | For `includeRules: true`, equals the number of returned results after merging and truncating to `limit`. | + +### `KnowledgeRuleHit` (Node) + +Extracted-rule hit returned by internal rule search helpers and surfaced in `MemoryKnowledgeSearchResult` when `includeRules` is `true`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ruleId` | `string` | Yes | Firestore document id for the extracted rule. | +| `sourceId` | `string` | No | Knowledge source that produced the rule. | +| `text` | `string` | Yes | Extracted rule text. | +| `score` | `number` | No | Cosine distance; lower is more relevant. Absent for recency-fallback hits. | +| `category` | `string` | No | Optional category from the rule document. | ### `MemoryListKnowledgeSourcesRequest` (Node) diff --git a/backend-sdks/node.mdx b/backend-sdks/node.mdx index 49ac55a7a..ebff725c9 100644 --- a/backend-sdks/node.mdx +++ b/backend-sdks/node.mdx @@ -3640,12 +3640,16 @@ await sdk.api.memory.deleteKnowledge({ sourceId: 'src-1' }); #### `searchKnowledge` -- Vector-search the workspace's knowledge chunks. +- Vector-search the workspace's knowledge chunks. Pass `includeRules: true` to also search extracted rules and merge rule hits with chunk hits in one relevance-ranked response. - Params: [MemoryKnowledgeSearchRequest](/api-reference/sdk/models/data-models#memoryknowledgesearchrequest-node) -- Returns: [VeltApiResponse](/api-reference/sdk/models/data-models#veltapiresponse-node) +- Returns: [VeltApiResponse](/api-reference/sdk/models/data-models#veltapiresponse-node) with [MemoryKnowledgeSearchResponse](/api-reference/sdk/models/data-models#memoryknowledgesearchresponse-node) under `result` ```ts -await sdk.api.memory.searchKnowledge({ query: 'color contrast guidelines' }); +await sdk.api.memory.searchKnowledge({ + query: 'image format requirements', + includeRules: true, + limit: 5, +}); ``` #### `listKnowledgeSources` diff --git a/release-notes/version-6/sdk-changelog.mdx b/release-notes/version-6/sdk-changelog.mdx index 82ed94d8f..2d2126a83 100644 --- a/release-notes/version-6/sdk-changelog.mdx +++ b/release-notes/version-6/sdk-changelog.mdx @@ -9,7 +9,7 @@ description: Release Notes of changes added to the core Velt SDK - `@veltdev/client` - `@veltdev/sdk` - + First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backward compatible — existing integrations work unchanged. @@ -19,6 +19,12 @@ First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backw - [**Core**]: New `preload()` methods on the `Velt` / `Snippyly` facade — 19 idempotent, non-throwing preloaders (e.g. `preloadComment()`) that eagerly fetch a single feature chunk and resolve once it registers. Use them to warm a chunk ahead of use, or to load tag-only features (`userInvite`, `userRequest`, `videoPlayer`) that have no element accessor. [Learn more →](/api-reference/sdk/api/api-methods) +- [**Memory**]: `POST /v2/memory/knowledge/search` now accepts an optional `includeRules` boolean. When `true`, the query is embedded once and run in parallel over both the knowledge-chunk corpus (reference content) and the extracted-rules corpus, merging both into a single relevance-ranked list so you no longer query chunks and rules in separate requests. Omitting or setting `includeRules` to `false` preserves the existing response shape byte-for-byte — no migration required. [Learn more →](/api-reference/rest-apis/v2/memory/knowledge/search) + +- [**Memory**]: When `includeRules` is `true`, every `knowledge/search` result carries a `kind` discriminator (`"chunk"` or `"rule"`). Rule hits additionally include the `ruleId` and an optional `category`, letting you distinguish extracted rules from reference chunks in the merged response. [Learn more →](/api-reference/rest-apis/v2/memory/knowledge/search) + +- [**Memory**]: `knowledge/search` now strictly validates `includeRules` — non-boolean values (e.g. the string `"true"` or `1`) are rejected with a `400` instead of being silently ignored. [Learn more →](/api-reference/rest-apis/v2/memory/knowledge/search) + ### Improvements - [**Core**]: `getXElement()` accessors (e.g. `getCommentElement()`) now return non-blocking lazy facades that kick off the chunk load and return immediately, preserving the synchronous, never-null contract — void calls are queued and flushed, promise calls await the load, and `on(...)` / observable subscriptions bridge through once the chunk lands. Each accessor also auto-enables its feature. Existing code compiles and runs unchanged. [Learn more →](/api-reference/sdk/api/api-methods) From 194b121c685fae8b40a31d9faff92f0a7967095e Mon Sep 17 00:00:00 2001 From: yoen-velt Date: Wed, 24 Jun 2026 14:13:03 -0400 Subject: [PATCH 3/3] Cloud Functions June 23 Pt. 2 --- ....0-beta.1-approval-engine-agent-tooling.md | 102 +++++++++ ...nt-3-impl-v6.0.0-beta.1-approval-engine.md | 99 +++++++++ ...alignment-v6.0.0-beta.1-approval-engine.md | 109 +++++++++ ...gent-7-qa-v6.0.0-beta.1-approval-engine.md | 56 +++++ ...proval-engine-agent-tooling-2026-06-23.mdx | 158 +++++++++++++ ai/agent-comments.mdx | 1 - ai/approval-engine/customize-behavior.mdx | 209 +++++++++--------- ai/approval-engine/overview.mdx | 8 +- ai/approval-engine/setup.mdx | 33 ++- ai/custom-agents.mdx | 105 +++++++++ .../definitions/create-definition.mdx | 88 +++++--- .../definitions/get-definition.mdx | 63 +++++- .../definitions/list-definitions.mdx | 24 +- .../definitions/update-definition.mdx | 42 ++-- .../v2/approval-engine/steps/resolve-step.mdx | 2 +- api-reference/sdk/models/data-models.mdx | 149 ++++++++++++- backend-sdks/node.mdx | 41 ++-- docs.json | 1 + release-notes/version-6/sdk-changelog.mdx | 36 ++- release-notes/version-6/upgrade-guide.mdx | 49 +++- 20 files changed, 1173 insertions(+), 202 deletions(-) create mode 100644 .claude/logs/agent-2-planning-v6.0.0-beta.1-approval-engine-agent-tooling.md create mode 100644 .claude/logs/agent-3-impl-v6.0.0-beta.1-approval-engine.md create mode 100644 .claude/logs/agent-6-alignment-v6.0.0-beta.1-approval-engine.md create mode 100644 .claude/logs/agent-7-qa-v6.0.0-beta.1-approval-engine.md create mode 100644 .claude/release-notes/input-v6.0.0-beta.1-approval-engine-agent-tooling-2026-06-23.mdx create mode 100644 ai/custom-agents.mdx diff --git a/.claude/logs/agent-2-planning-v6.0.0-beta.1-approval-engine-agent-tooling.md b/.claude/logs/agent-2-planning-v6.0.0-beta.1-approval-engine-agent-tooling.md new file mode 100644 index 000000000..dd225d712 --- /dev/null +++ b/.claude/logs/agent-2-planning-v6.0.0-beta.1-approval-engine-agent-tooling.md @@ -0,0 +1,102 @@ +# Release Update Plan for v6.0.0-beta.1 — Approval Engine & Agent Tooling (23 Jun 2026) + +> Scope note: This is the SECOND release note appended to the existing `` block. The base modular-SDK plan is in `agent-2-planning-v6.0.0-beta.1.md` — do NOT touch that work. This log covers ONLY the Approval Engine + Agent Tooling items. + +## Overview +- Release Type: Minor + one Breaking Change (within an existing beta version). +- Key Changes: Approval Engine unified `edges[]` contract (removes `onReject`/`loops[]`); read-only `compiled` block on DefinitionView; `waitAll`/`cancelOnQuorum` groups as edge sources; node cosmetic `name`/`description`; three new agent capabilities (`rest-api` context strategy, `mcp-tools` execution strategy, `docs-code-comparison` built-in agent); custom-agent config validation changes. +- Breaking Changes: YES — Approval Engine `edges[]` migration. Upgrade guide currently says "None — fully backward compatible," which is now WRONG and is the highest-priority gap. + +## Areas Requiring Updates + +### 1. Data Models +- Not applicable. Approval Engine + agent config schemas live in REST reference and conceptual pages, not `data-models.mdx` (SDK frontend types). No new SDK-facing types. SKIP. + +### 2. API Methods +- Not applicable. No new SDK hooks/facade methods. All surface is REST + conceptual. SKIP. + +### 3. Documentation / REST Reference + +**3a. Upgrade Guide — HIGHEST PRIORITY (breaking change)** +- File: `/Users/yoenzhang/Downloads/docs/release-notes/version-6/upgrade-guide.mdx` +- Changes: Replace "## Breaking Changes → None" (line 11-13) with an Approval Engine entry: the four-row `onReject`/`loops[]` → `edges[]` migration mapping (from raw input lines 14-22), the new `EdgeSchema` fields (`from`/`to`/`on`/`when`/`loop`), and the new `INVALID_ARGUMENT` error keys (raw input lines 31-41). Migration table belongs HERE, never in the changelog. +- Priority: High. + +**3b. Approval Engine REST — Create Definition** +- File: `/Users/yoenzhang/Downloads/docs/api-reference/rest-apis/v2/approval-engine/definitions/create-definition.mdx` +- Changes: Rewrite `edges` ParamField to the unified EdgeSchema (`from`/`to`/`on`/`when`/`loop`); REMOVE the `loops` ParamField (lines 62-64); add node `name`/`description` to node config; note group-as-edge-source (`waitAll`/`cancelOnQuorum`); update edge example + success response to show `compiled` block. Edge `when` is now valid only on `on:"custom"`. +- Priority: High. + +**3c. Approval Engine REST — Update Definition** +- File: `/Users/yoenzhang/Downloads/docs/api-reference/rest-apis/v2/approval-engine/definitions/update-definition.mdx` +- Changes: Mirror 3b edge/loops/node changes; remove `onReject`/`loops[]` references. +- Priority: High. + +**3d. Approval Engine REST — Get Definition** +- File: `/Users/yoenzhang/Downloads/docs/api-reference/rest-apis/v2/approval-engine/definitions/get-definition.mdx` +- Changes: Document read-only `compiled` block (`compiled.forwardEdges` + `compiled.loops`, schema from raw input lines 51-67) in the response; note authored `edges` echo `sourceEdges` byte-for-byte; remove `onReject`/`loops[]` echo references. +- Priority: High. + +**3e. Approval Engine REST — List Definitions** +- File: `/Users/yoenzhang/Downloads/docs/api-reference/rest-apis/v2/approval-engine/definitions/list-definitions.mdx` +- Changes: Note each row carries the `compiled` block; remove `onReject`/`loops[]` references. +- Priority: Medium. + +**3f. Approval Engine Conceptual — Customize Behavior (largest revision, 44KB)** +- File: `/Users/yoenzhang/Downloads/docs/ai/approval-engine/customize-behavior.mdx` +- Changes: Remove/rewrite the "Per-human-node `onReject` shorthand" section (lines 59-131), the `loops[]` region model, and "When to use a loop vs. the `onReject` shorthand" (line 259). Replace with unified `edges[]`: reject path = outgoing `on:"reject"` edge; loop-back = `on:"reject"` edge with `loop`; exhausted = sibling `on:"exhausted"` edge. Add group-as-edge-source semantics (waitAll unanimity, cancelOnQuorum quorum) and node `name`/`description`. Add the new `APPROVAL_EDGE_*` error keys. +- Priority: High. + +**3g. Approval Engine Conceptual — Setup** +- File: `/Users/yoenzhang/Downloads/docs/ai/approval-engine/setup.mdx` +- Changes: Rewrite `onReject.routeToNodeId` walkthrough (lines 62-105), the "Rejection handling: shorthand vs. loops" guidance (line 323), and the "Human node missing a reject path" troubleshooting (line 365) to the `edges[]` model. Fix anchor links pointing to removed customize-behavior sections. +- Priority: High. + +**3h. Approval Engine Conceptual — Overview** +- File: `/Users/yoenzhang/Downloads/docs/ai/approval-engine/overview.mdx` +- Changes: Verify no `onReject`/`loops[]` mentions remain; add `compiled` block and group-as-edge-source to feature list if applicable. Agent-3: grep first, edit only if hits. +- Priority: Low. + +### 4a. UI Customization — Wireframes +- Not applicable. Backend/REST release, no wireframes. SKIP. + +### 4b. UI Customization — Primitives +- Not applicable. No primitive components. SKIP. + +### 5. Upgrade Guide (breaking changes) +- Covered in 3a. Breaking changes go ONLY in upgrade-guide.mdx, never the changelog. + +## NO DOC TARGET — HUMAN DECISION REQUIRED (flag for Agent-3) + +The three new agent capabilities and the custom-agent config validation changes have NO existing public doc surface. Existing agent pages: `ai/agent-comments.mdx` (agents leaving comments, unrelated) and `api-reference/rest-apis/v2/agents/` (only `list-agent-executions.mdx`). Agent-1 deliberately omitted "Learn more" links for all 7 agent items. + +Items with no home: +- `rest-api` context-gathering strategy (raw input lines 83-97: `strategyOptions["rest-api"]`, `RestApiEndpoint` shape, SSRF guard, `{{restApiData}}`). +- `mcp-tools` execution strategy (raw input lines 99-110: `execution.mcpServers[]` shape, allowlists, max-turn cap). +- `docs-code-comparison` built-in agent (raw input lines 112-115). +- Custom-agent config validation: deep-merge partial updates; reject server-managed fields (`managedBy`, `metadata.*`) with per-key 400; `instructions` required for `ai`/`service+ai`/`stagehand-agent`; `listVersions` fail-closed secret redaction. + +Recommendation: warrants ONE new custom-agent configuration reference page (execution strategies + context-gathering strategies + built-in agents + config validation), e.g. `ai/agents/configuration.mdx` or `api-reference/rest-apis/v2/agents/`. DECISION DEFERRED to human — do NOT invent a page or fabricate URL paths this pass. If approved, Agent-3 also adds nav entries to `docs.json` (agents section near line 311 / 827). + +## DO NOT DOCUMENT (confirmed internal refactors) +ApiKeySecretCipher extraction; ProviderApiKeyService extraction; CryptoUtil/CRDT barrel-import migration; compileGraph-vs-linter validation split; ILlmProvider.askAIWithTools adapter internals; McpClientService internal plumbing. Email-digest determinism bug fix = changelog only, no reference doc. + +## Implementation Sequence +1. Upgrade guide breaking-change entry + migration table (High; blocks correctness of changelog link). +2. Approval Engine REST: create + update (edges/loops/node) (High). +3. Approval Engine REST: get + list (`compiled` block) (High/Medium). +4. Conceptual: customize-behavior rewrite, then setup (depends on customize-behavior anchors) (High). +5. Conceptual: overview grep-and-fix (Low). +6. Agent tooling: PAUSE — await human decision on new page before any agent-config docs. + +## Quality Checklist +- [ ] Breaking change moved to upgrade-guide.mdx ONLY (not changelog). +- [ ] Migration table (onReject/loops[] → edges[]) + new error keys in upgrade guide. +- [ ] All `onReject`/`loops[]`/`loopBack` references removed from REST + conceptual pages. +- [ ] `compiled` block documented on get/list/create responses. +- [ ] Group-as-edge-source (waitAll/cancelOnQuorum) + node name/description documented. +- [ ] No broken anchors after customize-behavior section removals (setup.mdx links). +- [ ] Code examples keep `React/Next.js` first, `Other Frameworks` second where tabs apply (REST pages are framework-agnostic JSON). +- [ ] Agent-tooling new-page decision surfaced to human; no invented URLs. +- [ ] Internal refactors NOT documented. +- [ ] Log written to `.claude/logs/agent-2-planning-v6.0.0-beta.1-approval-engine-agent-tooling.md`. diff --git a/.claude/logs/agent-3-impl-v6.0.0-beta.1-approval-engine.md b/.claude/logs/agent-3-impl-v6.0.0-beta.1-approval-engine.md new file mode 100644 index 000000000..627f69d0b --- /dev/null +++ b/.claude/logs/agent-3-impl-v6.0.0-beta.1-approval-engine.md @@ -0,0 +1,99 @@ +# Agent-3 Implementation Log — v6.0.0-beta.1 Approval Engine (23 Jun 2026) + +Scope: Approval Engine `edges[]` unification only. Agent-tooling features were CHANGELOG-ONLY per human orchestrator decision — NO new agents page, NO `docs.json` changes, NO documentation of `rest-api`/`mcp-tools`/`docs-code-comparison`/custom-agent validation. + +## Files changed (8) + +### A. `release-notes/version-6/upgrade-guide.mdx` — HIGHEST PRIORITY +- BEFORE: `## Breaking Changes` said "None — fully backward compatible." +- AFTER: Two bracket-prefixed bullets — `[**Modular SDK**]` (None) and `[**Approval Engine — unified edges[] contract**]` (breaking). Added an `### Approval Engine: migrate to edges[]` subsection under How to Upgrade with: the 4-row old→new migration mapping table, the new `EdgeSchema` field table (`from`/`to`/`on`/`when`/`loop`), and the 10-row `APPROVAL_*` compile-error-key table. Renamed the existing modular-SDK how-to heading to `## How to Upgrade the Modular SDK` to disambiguate. Migration detail lives here, NOT in the changelog. + +### B. `…/definitions/create-definition.mdx` +- `edges` ParamField rewritten to unified `EdgeSchema` (field table: `from`/`to` EdgeEndpoint, `on` enum default `always`, `when` only on `custom`, `loop` only on reject back-edge). Added the `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` rule note. +- REMOVED the `loops` ParamField. +- `nodes` ParamField: added optional cosmetic `name` (1–200) / `description` (≤2000). +- `groups` ParamField: added group-as-edge-source note (`waitAll` unanimity, `cancelOnQuorum` quorum, forward reject = dead edge, group-to-node fan-out). +- Node example: added `name`/`description`. Edge example: replaced single `when` example with 5 examples (approve/reject/reject+loop/exhausted/custom). Request example edges: split human→publish into `on:"approve"` + `on:"reject"`+`loop`. +- Success response + ResponseExample: added read-only `compiled` block (`forwardEdges`/`loops`) and a prose note that authored `edges` echo `sourceEdges` byte-for-byte. + +### C. `…/definitions/update-definition.mdx` +- Mirrored B: `edges` rewritten to EdgeSchema summary, `loops` ParamField REMOVED, `nodes` gained name/description, `groups` gained edge-source note, both response blocks gained `compiled`. + +### D. `…/definitions/get-definition.mdx` +- Success response JSON: added populated `compiled.forwardEdges` + `compiled.loops` examples; `edges` annotated as byte-for-byte `sourceEdges`. +- Added a `#### The compiled block` section with `` documenting `forwardEdges` (`from`/`to`/`role`/`when`/`fromGroupId?`/`toGroupId?`) and `loops` (`loopId`/`entryNodeId`/`bodyNodeIds`/`maxIterations`/`onExhausted`) — translated from the raw note's `CompiledGraph` TS interface into the page's ResponseField/table style. +- ResponseExample: added empty `compiled` block. + +### E. `…/definitions/list-definitions.mdx` +- Annotated `items` as carrying the read-only `compiled` block per row + prose note linking to get-definition `#the-compiled-block`. + +### F. `ai/approval-engine/customize-behavior.mdx` (largest) +- Node-configuration intro: added node `name`/`description` paragraph. +- REPLACED the entire `### Per-human-node onReject shorthand`, `### Strict-mode validation`, `### What desugaring rejects`, `### onReject.routeToNodeId on a joinOnQuorum group member is dead code`, `### Canonical form after desugaring`, and `## Edge gating expressions` block with: + - `### Rejection paths are edges` (short) + - `## Edge model` (field table + `### Reject, loop-back, and exhausted` + `### Custom predicates` [absorbs old edge-gating content] + `### Groups as edge sources` + `### The compiled view`). +- `## Loop regions`: rewrote intro + field table to describe SERVER-DERIVED loops (`compiled.loops[]` shape) instead of authored top-level `loops[]`. Removed the authored-`loops[]` JSON example, the authored field table, and the `### When to use a loop vs. the onReject shorthand` subsection. Kept `### Body-shape constraint`, `previousAttempts`, iteration-scoped groups, loop events (still accurate runtime behavior). +- `## Linter rules`: pruned obsolete authored-loop rows (`loop-duplicate-id`, `loop-entry-must-be-in-body`, `loop-body-member-missing`, `loop-body-unreachable-from-entry`, `loop-node-in-multiple-loops`, `loop-on-exhausted-route-to-not-found`, `loop-on-exhausted-route-to-in-body`). Kept `loop-body-must-have-single-terminal` + `loop-group-bounded-quorum-must-equal-expected` (reworded "derived"). Added `### Edge model validation errors` table with all 10 `APPROVAL_*` keys. + +### G. `ai/approval-engine/setup.mdx` +- Step 1 walkthrough: removed `onReject.routeToNodeId` from node config; reject path is now an `on:"reject"` edge. Removed redundant JSON-AST reject `when` edge. Added `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` note. Relinked from removed `#per-human-node-onreject-shorthand` → `#edge-model`. +- Mermaid diagram labels updated to `on: approve` / `on: reject`. +- "Going further": rewrote conditional-routing bullet (`on:"custom"`, `#custom-predicates`), parallel-groups bullet (added waitAll edge-source), and rejection-handling bullet ("reject edges & loop-backs", relinked to `#edge-model`/`#loop-regions`, removed `#when-to-use…` and `#edge-gating-expressions` dead anchors). +- Troubleshooting: "Human node missing a reject path" → `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` + add an `on:"reject"` edge. + +### H. `ai/approval-engine/overview.mdx` +- Grep found no `onReject`/`loops`/`compiled` hits. Edited the `### Edges` mental-model paragraph (was old `when`-only model) to describe the unified `on`-role model, reject edges, loop-back marking, group-as-edge-source, and the read-only `compiled` block; linked to `#edge-model`. + +### Out-of-plan fix +- `…/steps/resolve-step.mdx` line 75 referenced the now-removed authored `onIterationReject.when` field. Rewrote to the unified model: model the reject back-edge as `on:"custom"` with a `when` filtering on `decision` alone. Relinked `#loop-regions` → `#edge-model`. (Actively-wrong stale reference; same class as the in-scope removals.) + +## Broken anchors fixed (setup/resolve-step → customize-behavior) +- `#per-human-node-onreject-shorthand` (removed) → `#edge-model` +- `#edge-gating-expressions` (renamed) → `#custom-predicates` +- `#when-to-use-a-loop-vs-the-onreject-shorthand` (removed) → dropped, folded into `#edge-model`/`#loop-regions` +All referenced anchors verified to resolve to live headings (incl. new `#edge-model`, `#custom-predicates`, get-definition `#the-compiled-block`). + +## Verification +- Final grep: NO `onReject` / `loopBack` / `onIterationReject` / authored-`loops[]` references remain in any approval-engine page. Only surviving occurrences: the upgrade-guide migration table (intentional old→new mapping) and unrelated "loopback" in SSRF webhook text. All `"loops"` JSON literals are the read-only `compiled.loops` field. +- `compiled` block documented on create (4) + get (6, full field schema) + update (3); noted on list (2). +- Group-as-edge-source (waitAll unanimity / cancelOnQuorum quorum / group-to-node fan-out) documented in customize-behavior (dedicated section) + create + update + overview. +- Node `name`/`description` documented in customize-behavior + create + update. +- Code-fence parity even and all ``/`` tags paired in every edited file. + +## Confirmations +- Did NOT document any agent-tooling feature (`rest-api`, `mcp-tools`, `docs-code-comparison`, custom-agent validation). +- Did NOT create any new page. +- Did NOT touch `docs.json`. +- Did NOT touch the changelog (`sdk-changelog.mdx`); migration detail lives only in the upgrade guide. +- Did NOT expose internal-only identifiers (`compileGraph`/linter split, `ApiKeySecretCipher`, service class names, `INV-005`, `DEPLOY_ALL`, etc.). Public REST contract names (`compiled`, `forwardEdges`, `loops`, `edges`, EdgeSchema fields, `on` roles, `APPROVAL_*` error keys) ARE documented. + +## Needs human review / ambiguous +1. `resolve-step.mdx` edit was outside Agent-2's explicit file list but contained an actively-wrong authored-loop field reference. Fixed minimally; flag if it should be reverted or expanded. +2. Get/Create `compiled` examples use illustrative placeholder values for `forwardEdges`/`loops` (the raw note gives the TS interface, not a concrete JSON instance). Field shapes match the `CompiledGraph` interface; concrete example values are representative. +3. `compiled.loops[].onExhausted` documented as `{ routeToNodeId } | null` per the raw `CompiledGraph` interface — consistent across get-definition and the customize-behavior Loop regions table. + +--- + +## FOLLOW-UP (coordinator-relayed): backend SDK reference — old model still documented + +A repo-wide grep found the OLD edges/loops model still in the backend SDK reference (these files were not in Agent-2's plan). Updated to the unified `edges[]` model. Same constraints held: no agent-tooling docs, no `docs.json`, surgical edits. + +### node.mdx — `#### Node & Graph Types` inline TS block +- BEFORE: documented `ApprovalHumanNodeOnRejectLoopBack`, `ApprovalHumanNodeOnReject`, `ApprovalLoopRejectTrigger`, `ApprovalLoopExhaustionPolicy`, `ApprovalLoop`; `ApprovalHumanNodeConfig` carried `onReject?`; `ApprovalEdge` was `{ from: string; to: string; when?: string }`; node interfaces had no `name`/`description`. +- AFTER: removed all 5 now-deleted interfaces. `ApprovalHumanNodeConfig` keeps reviewerIds/reviewers/reviewerEmails/commentBody only (added inline comment: reject path is an outgoing `on:'reject'` edge). Added `type ApprovalEdgeEndpoint = string | { kind:'node'; nodeId } | { kind:'group'; groupId }` and rewrote `ApprovalEdge` to `{ from: ApprovalEdgeEndpoint; to: ApprovalEdgeEndpoint; on?: approve|reject|always|exhausted|custom (default always); when? (JSON-AST, only on custom); loop? { maxIterations 1..20 } (only on on:'reject' back-edge) }`. Added optional cosmetic `name?: string /* 1..200 */` + `description?: string /* max 2000 */` to `ApprovalAgentNode`, `ApprovalHumanNode`, `ApprovalWebhookNode`. Left `ApprovalQuorumPolicy`/`ApprovalGroup` as-is (waitAll/cancelOnQuorum/joinOnQuorum unchanged; groups-as-edge-sources is an edge concern, covered by `ApprovalEdgeEndpoint`). +- Also fixed the `createDefinition` method blurb "(nodes, edges, groups, loops, triggers)" → "(nodes, edges, groups, triggers)". +- Did NOT add compiled/CompiledGraph here (response types are not documented in this block — see data-models finding below). + +### data-models.mdx — `ApprovalCreateDefinitionRequest` (Node) +- BEFORE: had `| loops | ApprovalLoop[] | No | Loop definitions. |` row; `edges` description was generic. +- AFTER: removed the `loops` row. Updated `edges` description to note the unified `EdgeSchema` (`from`/`to`/`on`/`when`/`loop`) and that reject paths and loop-backs are `on:'reject'` edges. `ApprovalUpdateDefinitionRequest` (Node) extends create and added no separate edges/loops rows, so removing the create row covers update too. +- **compiled SKIPPED in data-models.mdx (with reason):** there is NO documented approval definition response-shape type in data-models.mdx. All approval definition methods (`createDefinition`/`updateDefinition`/`getDefinition`/`listDefinitions`) document `Returns: VeltApiResponse` (the generic Node response type) — there is no `ApprovalDefinitionView` / `ApprovalGetDefinitionResponse` entry with nodes/edges/groups to attach `compiled` to. Per the coordinator's explicit instruction, I did NOT invent a new anchor. The `compiled` block IS already documented on the REST reference pages (get/create/update/list) from the first pass. + +### python.mdx — verify only (NO edit) +- Confirmed: the Workflow examples (createDefinition/updateDefinition/etc., ~lines 4684-4752) use `edges=[]` and inline node dicts. Full-file grep for `onReject`/`loopBack`/`loops=`/`"loops"`/`ApprovalLoop`/`onIterationReject` → zero hits. The `edges=[]` examples are illustrative empty-edge examples (runnable); left as-is per instruction. + +### Final grep (backend-sdks/ + data-models.mdx) +Word-boundary grep for `onReject|loopBack|ApprovalLoop|ApprovalLoopRejectTrigger|ApprovalLoopExhaustionPolicy|ApprovalHumanNodeOnReject|ApprovalHumanNodeOnRejectLoopBack|onIterationReject` → **zero matches** (exit 1). The only `*reject*` substrings remaining are the unrelated AI-suggestion events (`SUGGESTION_REJECTED`/`suggestionRejected`). node.mdx code fences balanced; the Node & Graph Types ```ts block opens/closes correctly. + +### IMPORTANT ASSUMPTION FLAG (for human) +These backend SDK reference edits assume the `@veltdev/node` and `velt_py` SDK request types track the backend Cloud Functions contract for this release. If those SDKs have NOT yet shipped the `edges[]` migration (i.e. their published types still accept `onReject`/`loops[]`), then node.mdx / data-models.mdx now describe the new model AHEAD of the actual SDK surface, which would mislead SDK callers. Verify the shipped `@veltdev/node` / `velt_py` versions expose the unified `ApprovalEdge` (with `on`/`loop`) and reject `onReject`/`loops[]` before publishing. python.mdx was left unedited, so no python-side drift was introduced. diff --git a/.claude/logs/agent-6-alignment-v6.0.0-beta.1-approval-engine.md b/.claude/logs/agent-6-alignment-v6.0.0-beta.1-approval-engine.md new file mode 100644 index 000000000..bc3fb8d5e --- /dev/null +++ b/.claude/logs/agent-6-alignment-v6.0.0-beta.1-approval-engine.md @@ -0,0 +1,109 @@ +# Agent-6 Alignment Log — v6.0.0-beta.1 Approval Engine & Agent Tooling (23 Jun 2026) + +Scope: alignment-only verification of Agent-3's `edges[]` unification + `compiled` block work across the full docs tree. Agents 4 & 5 were no-ops (backend/REST release, no wireframes/primitives). + +## Result: ZERO edits required + +Agent-3's implementation was fully consistent. No genuine drift, broken anchors, banned identifiers, or error-key typos found. All findings below are PASS confirmations. + +## Files scanned + +- `release-notes/version-6/sdk-changelog.mdx`, `upgrade-guide.mdx` +- `api-reference/rest-apis/v2/approval-engine/definitions/{create,update,get,list}-definition.mdx` +- `api-reference/rest-apis/v2/approval-engine/steps/resolve-step.mdx`, `executions/{dispatch-execution,get-execution-events}.mdx` +- `ai/approval-engine/{overview,setup,customize-behavior}.mdx` +- `backend-sdks/node.mdx`, `backend-sdks/python.mdx`, `api-reference/sdk/models/data-models.mdx` +- Adjacent flagged files: `webhooks/advanced.mdx`, `get-started/agentic-overview.mdx`, `ai/agent-comments.mdx` +- `docs.json` (verify-only, not modified) + +## Verification results + +### 1. Old-model references — CLEAN +Case-sensitive word-boundary grep for `onReject` / `loopBack` / `onIterationReject` / `ApprovalLoop` / `ApprovalHumanNodeOnReject` across `ai/`, `api-reference/`, `backend-sdks/`: +- **Zero hits** in approval-engine REST pages, conceptual pages, and backend SDK pages. +- Surviving mentions are all intentional: + - `upgrade-guide.mdx` migration table (old→new mapping — required). + - `sdk-changelog.mdx` line 18 — prose naming the removed concepts to explain the breaking change (required). +- False-positive substrings correctly left alone: `loopback` (SSRF network term in webhook validation, setup/customize-behavior/dispatch-execution), `compiled.loops[]` (new read-only field), `SUGGESTION_REJECTED`/`suggestionRejected` (unrelated comment-suggestion feature). + +### 2. APPROVAL_* error keys — EXACT MATCH (10/10) +Every key in the docs matches the raw note's set byte-for-byte. No typo'd or invented constants: +``` +APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN +APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM +APPROVAL_EDGE_LOOP_REQUIRES_REJECT +APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR +APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP +APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING +APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH +APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN +APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP +APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM +``` +(The bare `APPROVAL_EDGE_*` glob token exists only in the raw note, not in docs.) + +### 3. Banned internal identifiers — ABSENT +`compileGraph`, `ApprovalDefinitionService`, `CustomAgentFirestoreService`, `ApiKeySecretCipher`, `ProviderApiKeyService`, `CryptoUtil`, `ILlmProvider`, `askAIWithTools`, `McpClientService`, `INV-005`, `INV-022`, `DEPLOY_ALL`: **zero hits** in any docs page (only in raw note + pipeline logs). + +`linter` (13 occurrences) is NOT banned — the ban targets the internal "compileGraph-vs-linter split mechanics". The user-facing "linter rules / linter code / linter validation" concept predates this release (present since v5.0.2-beta.31) and is consistent. Kept. + +### 4. Canonical terminology — CONSISTENT +- `on` role values: only `approve` / `reject` / `always` / `exhausted` / `custom` appear. No stray roles. +- Compiled-vs-authored distinction maintained correctly: authored edges use `on`; `compiled.forwardEdges` entries use `role` (per the `CompiledGraph` interface). get-definition's field table even labels it "The authored `on` role." +- EdgeEndpoint `kind:"node"` / `kind:"group"` spelling consistent (node.mdx uses single quotes per its TS-block style — acceptable). +- Group policies: `waitAll` / `cancelOnQuorum` / `joinOnQuorum` — exact casing everywhere in prose. Lowercase forms appear only inside pre-existing linter-rule slug names (`group-cancelonquorum-requires-quorum-lt-expected`, `group-joinonquorum-members-must-share-successors`) and the `..._REQUIRES_JOINONQUORUM` error-key constant — all correct by convention. +- `routeToNodeId` appears only in `compiled.loops[].onExhausted` and the upgrade-guide migration table — never as an authored edge field. + +### 5. `compiled` block schema — MATCHES CompiledGraph interface +get-definition `#### The compiled block` field tables match the raw note field-for-field: +- `forwardEdges`: `from`, `to`, `role`, `when` (`JsonAst | null`), `fromGroupId?`, `toGroupId?` +- `loops`: `loopId`, `entryNodeId`, `bodyNodeIds` (string[]), `maxIterations`, `onExhausted` (`{ routeToNodeId } | null`) + +`sourceEdges` used only as the documented echo contract concept ("authored `edges` echo `sourceEdges` byte-for-byte") in caller-facing phrasing — permitted usage. + +### 6. Cross-reference links / anchors — ALL RESOLVE +`Learn more →` target files all exist: +- `/release-notes/version-6/upgrade-guide` ✓ +- `/api-reference/rest-apis/v2/approval-engine/definitions/get-definition` ✓ +- `/ai/approval-engine/customize-behavior` ✓ +- `/api-reference/rest-apis/v2/approval-engine/definitions/create-definition` ✓ + +In-page anchors to customize-behavior NEW heading slugs — all resolve to live headings: +- `#edge-model` → `## Edge model` ✓ +- `#custom-predicates` → `### Custom predicates` ✓ +- `#loop-regions` → `## Loop regions` ✓ +- `#node-configuration`, `#parallel-groups-and-quorum-policies`, `#linter-rules`, `#event-reference`, `#webhook-retry-policy`, `#sla-and-breach-handling`, `#agent-nodes`, `#onquorummet-policies`, `#specific-must-approve-quorum`, `#canonical-codes` ✓ +- get-definition `#the-compiled-block` → `#### The compiled block` (referenced from list-definitions + customize-behavior) ✓ +- No dead anchors to removed sections (`#per-human-node-onreject-shorthand`, `#edge-gating-expressions`, `#when-to-use-a-loop...`) anywhere. ✓ +- data-models `#approvalcreatedefinitionrequest-node` refs from node.mdx — N/A (Agent-3 confirmed no separate response-shape anchor; not introduced). + +### 7. Agent-tooling — NO DOC SURFACE (as mandated) +- All 6 `[**Agents**]` changelog bullets (lines 38, 40, 42, 56, 66, 68) are **link-less**. Confirmed: zero `[...](...)` links in any Agents bullet. +- No `ai/agents/` directory exists. `api-reference/rest-apis/v2/agents/` contains only the pre-existing `list-agent-executions.mdx`. +- `docs.json` has no agent-tooling nav entries and is NOT in the modified-files set (untouched). +- No `restApiData` / `mcp-tools` / `docs-code-comparison` / `mcpServers` references leaked into any prose doc page. + +### 8. Modified-files set — matches Agent-3 exactly +`git status` shows exactly the 12 files Agent-3 reported as changed. No stray/new doc files. The 3 untracked files are pipeline inputs/logs. + +## Canonical term list for Agent-7 final QA pass + +Enforce EXACT forms; flag drift: +- Edges: `edges[]`, `EdgeSchema`; fields `from`, `to`, `on`, `when`, `loop`. +- `on` roles (quoted): `"approve"`, `"reject"`, `"always"` (default), `"exhausted"`, `"custom"`. +- EdgeEndpoint kinds: `{ kind: "node", nodeId }`, `{ kind: "group", groupId }` (single quotes acceptable only inside node.mdx TS block). +- Compiled view: `compiled`, `compiled.forwardEdges`, `compiled.loops`. forwardEdges entry fields: `from`, `to`, `role`, `when`, `fromGroupId?`, `toGroupId?`. loop fields: `loopId`, `entryNodeId`, `bodyNodeIds`, `maxIterations`, `onExhausted`. +- Group policies: `waitAll`, `cancelOnQuorum`, `joinOnQuorum`. +- Echo contract term: `sourceEdges` (caller-facing echo phrasing only). +- Error keys: the 10 `APPROVAL_*` constants listed in §2 — spell EXACTLY. +- Node cosmetic fields: `name` (1–200), `description` (max 2000). +- LEAVE ALONE (unrelated): `suggestionRejected` / `SuggestionRejectEvent` / `ActionReject` / `SUGGESTION_REJECTED` (comment-suggestion feature); `loopback` (SSRF network term). +- DO NOT introduce: banned internal identifiers in §3; agent-tooling pages/links/nav. + +## Needs human review + +1. **SDK contract assumption (carried from Agent-3, unresolved):** `backend-sdks/node.mdx` + `data-models.mdx` now describe the unified `ApprovalEdge` (`on`/`loop`, no `onReject`/`loops[]`). This assumes the published `@veltdev/node` SDK has shipped the `edges[]` migration in lockstep with the Cloud Functions contract. If the SDK still accepts `onReject`/`loops[]`, these pages describe the model ahead of the shipped SDK surface. Verify before publish. (`python.mdx` left unedited — no python-side drift.) +2. **`compiled` example values are illustrative** (carried from Agent-3): get/create `compiled.forwardEdges`/`loops` examples use representative placeholder values (raw note gives the TS interface, not concrete JSON). Field shapes are correct; concrete values are representative. + +## Handoff to Agent-7 +Aligned docs verified clean. Change map above is the source of truth for Agent-7's repo-wide QA terminology pass. No corrective edits were needed this pass. diff --git a/.claude/logs/agent-7-qa-v6.0.0-beta.1-approval-engine.md b/.claude/logs/agent-7-qa-v6.0.0-beta.1-approval-engine.md new file mode 100644 index 000000000..f69a98fde --- /dev/null +++ b/.claude/logs/agent-7-qa-v6.0.0-beta.1-approval-engine.md @@ -0,0 +1,56 @@ +# Agent-7 QA Log — v6.0.0-beta.1 Approval Engine & Agent Tooling (23 Jun 2026) + +Independent final QA pass. Scope: 12 changed files (working tree). **Verdict: PASS. Zero corrective edits made.** One PRE-EXISTING dead anchor flagged (out of this release's scope — not fixed, per constraints). + +## Tooling note +Multi-directory `rg` with a space-separated shell variable + `\b` anchors silently returned 0 hits in this environment (false clean). All scans below were re-run with reliable `rg`/`grep` over explicit single paths or bash arrays. Trust these results, not any earlier multi-root `rg` runs. + +## 1. Old/removed-model survivor scan (case-sensitive, all doc roots) +| Term | Survivors | Verdict | +| --- | --- | --- | +| `onReject` | upgrade-guide (5 lines: migration table + prose), sdk-changelog v6 ln18; all other hits are `suggestionRejected`/`SuggestionRejectEvent`/`SUGGESTION_REJECTED` substring matches in agent-comments/api-methods/data-models/v5-changelog | OK — only intentional + LEAVE-ALONE feature | +| `loopBack` | upgrade-guide migration table only (ln16,29,31) | OK — intentional | +| `loops[]` (authored) | upgrade-guide + sdk-changelog v6 (named as removed) | OK — intentional | +| `compiled.loops[]` | customize-behavior ln209 | OK — NEW read-only model, leave alone | +| `ApprovalLoop` / `ApprovalLoopRejectTrigger` / `ApprovalLoopExhaustionPolicy` / `ApprovalHumanNodeOnReject` / `onIterationReject` | **zero hits** | OK — fully removed | + +Confirmed only surviving old-model references are the upgrade-guide migration table + the single changelog breaking-change bullet. node.mdx/data-models.mdx correctly dropped the removed interfaces and `loops` field. + +## 2. The 10 `APPROVAL_*` error keys — independent re-verification +Extracted all keys from raw note (minus the `APPROVAL_EDGE_` glob token), upgrade-guide.mdx, customize-behavior.mdx. Byte-exact `diff`: **all three sets IDENTICAL (10/10)**. The long ones re-checked individually — all correct: +`APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING`, `APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR`, `APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP`, `APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM`, `APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP`, `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH`, `APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN`, `APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN`, `APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM`, `APPROVAL_EDGE_LOOP_REQUIRES_REJECT`. +(customize-behavior shows 11 occurrences because `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` appears in both the prose at ln63 and the error table — not a duplicate key.) + +## 3. Canonical NEW-term presence (approval-engine surface) +`edges[]` 8/4f · `EdgeSchema` 3/3f · `EdgeEndpoint` 11/5f · `compiled.forwardEdges` 6/6f · `compiled.loops` 6/5f · `fromGroupId` 1 · `toGroupId` 1 (both only in get-definition compiled table) · `waitAll` 11 · `cancelOnQuorum` 20 · `joinOnQuorum` 23 · `sourceEdges` 6/4f · `ApprovalEdge`/`ApprovalEdgeEndpoint` present in node.mdx. `on:` roles scanned: only `approve/reject/always/exhausted/custom` — zero stray roles. Group policies: exact camelCase everywhere (lowercase only inside linter slugs / `..._JOINONQUORUM` constant). + +## 4. Link / anchor verification +- All 18 cross-ref target FILES exist (changelog `[Learn more →]`, setup/customize-behavior/get/create links, etc.). ✓ +- All 13 `customize-behavior#…` anchors resolve to live headings. ✓ +- `get-definition#the-compiled-block` ✓; `overview#inbound-webhook-handler`, `overview#scope` ✓; `setup#step-4-get-the-outcome` ✓. +- node.mdx → data-models `#approvalcreatedefinitionrequest-node` / `#veltapiresponse-node` anchors unchanged & valid. ✓ +- **FLAG (pre-existing, NOT this release):** `customize-behavior.mdx:306` → `setup#step-3-configure-your-webhook-receiver` is DEAD. setup Step 3 is "Find the pending step and record a decision"; webhook-receiver setup lives under Step 4 (`#step-4-get-the-outcome`). Present in HEAD since v5.0.2-beta.31 (#201) in BOTH the ref line and the setup headings. Left unedited per "flag, don't alter pre-existing content." Suggested fix for human: repoint to `#step-4-get-the-outcome`. + +## 5. MDX well-formedness (all 12 edited files) +Code fences even on every file. JSX components balanced: `ParamField` (15/15 create & update, 6/6 get, etc.), `ResponseField` 3/3, `Expandable`, `ResponseExample`, `Note`, `Warning`, `Tabs` 3/3, `Update` 1/1, `Card`/`CardGroup`, `Accordion`/`AccordionGroup` all matched. node.mdx + REST `ts`/`JSON` blocks balanced (node.mdx approval TS block opens ```ts / closes ```). ✓ + +## 6. Banned internal identifiers +Scanned all doc roots (ai, api-reference, backend-sdks, release-notes, get-started, security, async/realtime-collab, ui-customization) for: `compileGraph`, `ApiKeySecretCipher`, `ProviderApiKeyService`, `McpClientService`, `ILlmProvider`, `askAIWithTools`, `CryptoUtil`, `INV-005/022`, `DEPLOY_ALL`, plus service-class names (`*Service`, `*Validator`, `assertSafeOutboundUrl`, `redactConfigSecrets`, `stripServerManagedMetadata`). **Zero hits in published docs.** ✓ + +## 7. Agent-tooling = changelog-only (as mandated) +- Agent-tooling internals (`restApiData`, `mcp-tools`, `docs-code-comparison`, `execution.mcpServers`, `stagehand-agent`, `DOCS_CODE_COMPARISON`, `CommonAgentResponse`) appear ONLY in v6 sdk-changelog Agents bullets. (`mcpServers` hits in get-started/mcp-installer + plugins are pre-existing, unrelated MCP-installer JSON.) +- All 6 `[**Agents**]` changelog bullets are **link-less** (verified). ✓ +- No `ai/agents/` dir; `api-reference/.../agents/` has only pre-existing `list-agent-executions.mdx`. `docs.json` untouched (not in changed set). ✓ + +## 8. Round-trip / default-behavior claims +"authored `edges` echo `sourceEdges` byte-for-byte" + "`compiled` is read-only / additive" stated consistently across create / get / customize-behavior. `on` defaults to `"always"`; `compiled` schema matches the `CompiledGraph` interface field-for-field. React tab uses `client.*`, Other Frameworks uses `Velt.*` (upgrade-guide modular section). ✓ Changelog order Breaking → New Features → Improvements → Bug Fixes; date June 23 2026 consistent; prior Memory release (4 bullets) intact, no clobber. + +## Flags for human (carry forward) +1. **Pre-existing dead anchor** `customize-behavior.mdx:306` → `setup#step-3-configure-your-webhook-receiver` (correct target `#step-4-get-the-outcome`). Not this release; not fixed. **Recommend a separate cleanup PR.** +2. **SDK-contract-ahead-of-shipped-SDK** (from Agent-3/6): node.mdx + data-models.mdx describe the unified `ApprovalEdge` (`on`/`loop`, no `onReject`/`loops[]`). Assumes `@veltdev/node` shipped the `edges[]` migration in lockstep with the Cloud Functions contract. Verify before publish. (python.mdx untouched — correct.) +3. **`compiled` example values are placeholders** (loopId `"..."`, JsonAst `{}`): shapes correct, concrete values illustrative. +4. June 22-vs-23 date: resolved (23 Jun everywhere). +5. docs.json: no approval/agents nav change this release — intentional and untouched. + +## Verdict +**PASS** — terminology consistency for v6.0.0-beta.1 Approval Engine is correct; the unified `edges[]` model is applied uniformly; the only old-model references are the intentional migration table + changelog bullet. Zero edits required. Pipeline may proceed to Plugin Agent 1 (skills delta extraction). The one dead anchor is pre-existing and explicitly out of scope. diff --git a/.claude/release-notes/input-v6.0.0-beta.1-approval-engine-agent-tooling-2026-06-23.mdx b/.claude/release-notes/input-v6.0.0-beta.1-approval-engine-agent-tooling-2026-06-23.mdx new file mode 100644 index 000000000..b68726d2c --- /dev/null +++ b/.claude/release-notes/input-v6.0.0-beta.1-approval-engine-agent-tooling-2026-06-23.mdx @@ -0,0 +1,158 @@ +Release - 23 Jun 2026 - Approval Engine & Agent Tooling +(shared-firebase-function / Cloud Functions backend; to be appended to the v6.0.0-beta.1 release note) + +========================================================= +BREAKING CHANGES +========================================================= +What: Removed the onReject per-human-node shorthand and the authored loops[] array from the Approval Engine contract. All workflow transitions — approve routing, reject routing, group fan-out/in, and loop-backs — are now expressed as a single unified edges[] array. Human nodes no longer carry any rejection config; their reject path is expressed purely by outgoing edges. + +Why: The prior model had two ways to express rejection (per-node onReject shorthand + top-level loops[]) that desugared at write time into a canonical form consumers never saw. This created a mismatch between what callers POSTed and what the GET echoed back. The unified edge model closes the round-trip symmetry gap: POST.edges === GET.edges. + +Impact: Any definition that uses HumanNodeConfig.onReject (either routeToNodeId or loopBack) or a top-level loops[] array will now be rejected at write time with INVALID_ARGUMENT. These callers must migrate to the equivalent edges[] form before writing new definitions. Existing in-flight executions and already-stored definitions are unaffected — the runtime still consumes compiled edges + derived loops unchanged. + +Migration: +Old form -> New edge equivalent + "onReject": { "routeToNodeId": "rework-notice" } + -> { "from": "human-review", "to": "rework-notice", "on": "reject" } + "onReject": { "loopBack": { "toNodeId": "agent-draft", "maxIterations": 3 } } + -> { "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } + "loops": [{ "loopId": "...", "entryNodeId": "agent-draft", "bodyNodeIds": [...], "maxIterations": 3 }] + -> { "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } + onReject.loopBack.onExhausted.routeToNodeId + -> sibling { "from": "human-review", "to": "escalate", "on": "exhausted" } edge + +New EdgeSchema fields: + from EdgeEndpoint yes Bare string or { kind:"node", nodeId } or { kind:"group", groupId } + to EdgeEndpoint yes Same + on "approve" | "reject" | "always" | "exhausted" | "custom" no (default "always") Semantic role; approve/reject auto-compile their predicates + when JSON-AST string only for on:"custom" Custom predicate. Invalid on any other edge. + loop { maxIterations: 1–20 } only on on:"reject" back-edge Marks the reject edge as a loop-back; server derives the loop region. + +New compileGraph error keys (all INVALID_ARGUMENT): + APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN on:"custom" without a non-empty when + APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM when supplied on a non-custom edge + APPROVAL_EDGE_LOOP_REQUIRES_REJECT loop on a non-reject edge + APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR reject+loop whose to is not an ancestor of from + APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP on:"reject" to an ancestor without loop (unmarked cycle) + APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING on:"exhausted" with no sibling reject+loop from the same from + APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH Human node with no outgoing on:"reject" edge + APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN Both endpoints are group containers + APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP Forward on:"reject" from a joinOnQuorum/cancelOnQuorum group (dead edge — those policies fan out only on approval-quorum) + APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM reject+loop back-edge from a non-joinOnQuorum group + +========================================================= +NEW FEATURES +========================================================= +1. compileGraph / DefinitionView.compiled +What: Added compileGraph to ApprovalDefinitionService, exposing a read-only compiled block on every DefinitionView (create / get / list rows). compiled.forwardEdges is the runtime forward edge list the engine actually drives execution from (group endpoints expanded, on roles compiled to whenAst). compiled.loops is the derived loop region list. The authored edges still echo sourceEdges byte-for-byte. +Why: Clients previously had to re-implement the compiler client-side to render a visual graph or understand the runtime behavior. compiled removes that burden. +Impact: Any client rendering a workflow graph can read DefinitionView.compiled.forwardEdges directly — per-member compiled edges tagged with fromGroupId/toGroupId to collapse at group boundaries — instead of re-implementing group expansion. compiled.loops exposes server-derived loop regions. Both are additive and read-only. + +interface CompiledGraph { + forwardEdges: Array<{ + from: string; // resolved node id + to: string; // resolved node id + role: "approve" | "reject" | "always" | "exhausted" | "custom"; + when: JsonAst | null; // deterministic predicate AST; null for "always" + fromGroupId?: string; // set when the authored `from` was a group + toGroupId?: string; // set when the authored `to` was a group + }>; + loops: Array<{ + loopId: string; + entryNodeId: string; + bodyNodeIds: string[]; + maxIterations: number; + onExhausted: { routeToNodeId: string } | null; + }>; +} + +2. waitAll / cancelOnQuorum groups as edge sources +What: waitAll groups can now be edge sources (from: { kind:"group", groupId }), giving them a collective approve/reject fan-out with unanimity decision logic. cancelOnQuorum groups can also be edge sources (collective approve fan-out on quorum). Group-to-node fan-out (to: { kind:"group", groupId }) is accepted for all quorum policies. +Why: Before this release, joinOnQuorum was the only policy that could fire a single collective successor. waitAll had no collective branch — callers had to rely on per-member fan-out, which fires N times. The unanimity model lets a workflow express "all reviewers must finish, then take one approve path or one reject path as a unit." +Impact: +- waitAll edge-source: all members must terminate; collective decision is unanimity — approve if every member approved, else reject. Authors can provide both an on:"approve" and an on:"reject" branch; exactly one fires. Successor step ID: group___to__. +- cancelOnQuorum edge-source: fires one collective approve-successor on approval-quorum (same as joinOnQuorum). Forward on:"reject" from a cancelOnQuorum group is rejected (dead edge). +- to: group fan-out expands to one compiled edge per group member for all policies. +- Terminal status is deterministic: when a waitAll edge-source group's collective-reject branch fires and its successor completes, the execution rolls up to completed — a routed collective reject is NOT a failed run. A member step whose rejection is handled by the group's collective edge is surfaced as completed with output.decision="reject", never as failed. + +3. Node cosmetic name / description +What: Nodes now accept optional cosmetic name (1–200 chars) and description (max 2000 chars) fields, echoed verbatim in NodeView. +Why: Clients building visual graph editors needed a stable label per node that survives round-trips. +Impact: Any workflow definition can now attach human-readable labels to nodes without affecting runtime behavior. Both fields are additive and schema-validated. + +4. "rest-api" context-gathering strategy for custom agents +What: When configured, the agent fetches a list of customer-defined REST endpoints at execution time and injects the results into the prompt via the {{restApiData}} template variable. +Why: Agents previously could only gather context from the page being analyzed (web-page-text, screenshot, etc.). Many use cases require pulling live data from a customer's own APIs — user profiles, CRM records, billing status — before the AI executes. +Impact: Custom agents can now be configured with up to 10 REST endpoints per execution. Endpoint auth secrets are encrypted at rest using ApiKeySecretCipher and redacted on every read path. Templating ({{variable}}, {{userContext.X}}, {{variables.X}}) is supported in url, headers, query, and body. SSRF-guarded via assertSafeOutboundUrl (blocks internal/loopback/link-local hosts). +strategyOptions["rest-api"] shape: + endpoints RestApiEndpoint[] yes 1–10 endpoints + maxResponseBytes number no Per-endpoint cap. Default 1 000 000, max 5 000 000. +RestApiEndpoint fields: + url string yes SSRF-validated, supports {{variable}} templating + description string yes Surfaced in the prompt alongside URL + response + method "GET"|"POST"|"PUT"|"PATCH"|"DELETE" no Default "GET" + auth RestApiAuthConfig no none|bearer|basic|header. Secret fields encrypted at rest, redacted on read. + cacheTtlSeconds number no In-process cache TTL per Cloud Function instance. Default 0 (always fresh). Max 86400. + timeoutMs number no Per-request timeout. Default 10 000, max 30 000. + responsePath string no Dot-path to extract a sub-field of the JSON response + +5. "mcp-tools" execution strategy for custom agents +What: The agent connects to one or more customer-configured MCP servers (remote HTTP/Streamable HTTP transport), runs a multi-turn tool-calling loop, and returns a structured CommonAgentResponse. +Why: The previous ai execution strategy was single-shot — one LLM call, no tool loop. The MCP strategy lets agents query external tools (search, fetch, execute) across multiple turns before producing a final structured answer, enabling more complex reasoning patterns like the Docs Code Verifier agent. +Impact: Custom agents can now declare execution.executionStrategy: "mcp-tools" with execution.mcpServers[] config. MCP auth headers are encrypted at rest via ApiKeySecretCipher and redacted on read. Per-server tool allowlists, per-turn redaction, and a max-turn cap prevent runaway loops. Both Claude (stop_reason: "tool_use" loop) and Gemini (response.functionCalls() loop) provider adapters are implemented via the new ILlmProvider.askAIWithTools interface. +execution.mcpServers[] shape: +[ + { + "url": "https://docs.example.com/mcp", + "auth": { "type": "header", "headers": { "X-Api-Key": "sk_xxx" } }, + "allowedTools": ["search_docs", "fetch_page"] + } +] + +6. docs-code-comparison built-in agent +What: Added docs-code-comparison built-in agent config. Uses the MCP tool loop to verify code snippets on a landing page against the docs site, reporting any mismatches as annotations. +Why: The built-in agent suite previously had no documentation-verification capability. +Impact: Available as a built-in agent (DOCS_CODE_COMPARISON_CONFIG). Uses web-page-text context gathering (with includeCodeBlocks: true) + mcp-tools execution against a remote docs MCP server. Provider-agnostic (works with Claude and Gemini). + +7. ApiKeySecretCipher extraction [INTERNAL REFACTOR — no public API surface] +What: Extracted ApiKeySecretCipher into core/crypto/api-key-secret-cipher.ts. Centralizes the ${apiKey}:${salt} AES-GCM encrypt/decrypt pattern previously duplicated across ProviderApiKeyService and RestApiConfigService. +Why: The pattern was independently implemented multiple times with no shared contract. +Impact: All callers delegate to ApiKeySecretCipher.encrypt / .decrypt. Per-feature salt strings caller-supplied and byte-stable (INV-005). Any non-constants.ts change under core/ triggers DEPLOY_ALL=true. CryptoUtil moved from shared/utils/ into core/crypto/ to satisfy the core → shared layering constraint. + +8. ProviderApiKeyService [INTERNAL REFACTOR — no public API surface] +What: Added ProviderApiKeyService to shared/ai/, extracting customer LLM API-key lookup/decryption from RewriterService into a reusable shared service. +Why: With agents and the MCP strategy needing customer provider keys, the logic needed to be shared. +Impact: RewriterService, agents, and future LLM consumers call ProviderApiKeyService.getCustomerAiKey() with { provider, workspaceObject }. Fallback logic unchanged. + +========================================================= +IMPROVEMENTS +========================================================= +1. compileGraph-vs-linter split enforced [INTERNAL — validation categorization] +Structural validations dependent on authored edge provenance run in compileGraph BEFORE loop extraction; DAG-integrity and group/loop-shape checks run in the linter on the compiled forward-only graph. Validation errors are now reliably categorized: authored-provenance errors from compileGraph (new APPROVAL_EDGE_* constants), graph-structure errors from the linter. No behavior change for definitions that pass validation. + +2. ILlmProvider.askAIWithTools (Claude + Gemini) [INTERNAL — provider adapter] +Both ClaudeProvider and GeminiProvider now implement askAIWithTools(request, executor), running a capped multi-turn tool loop. Usage metadata accumulated across turns. askAIWithTools is optional on ILlmProvider — callers check presence and fail with AI_TOOLS_NOT_SUPPORTED. Claude uses stop_reason:"tool_use"/tool_result loop; Gemini uses response.functionCalls()/functionResponse parts. Both cap turns, force a tool-free finishing turn at cap, forward tool errors back to the model. + +3. Deep merge + redactConfigSecrets [PARTLY DEVELOPER-VISIBLE: security redaction] +CustomAgentFirestoreService.updateAgentConfig now performs a deep merge instead of shallow merge. McpConfigService and RestApiConfigService expose static redactConfigSecrets used by AgentsServiceV2.listVersions and getAgentConfig to strip encrypted auth ciphertext ({ __enc }) before returning version snapshots. Why: shallow merge overwrote nested config fields (e.g. execution.mcpServers); version snapshots contained raw { __enc } ciphertext (security gap F1, AC-004). Impact: partial updateAgentConfig preserves nested fields; listVersions redacts all encrypted secrets (fail-closed — rethrows if redaction throws). + +4. CustomAgentConfigValidator rejects server-managed fields [DEVELOPER-VISIBLE validation] +Now rejects server-managed fields (managedBy, metadata.type, metadata.category, metadata.internal, metadata.apiKey) in create and update-version payloads with a per-key 400 INVALID_ARGUMENT. Defense-in-depth strip at the Firestore write layer (stripServerManagedMetadata). Why: clients could set metadata.internal=true to hide an agent, or spoof managedBy:"velt" — privilege escalations. Existing agents unaffected. + +5. McpClientService improvements [INTERNAL-ish] +Improved error handling, structured logging, per-tool allowlist enforcement, client metadata header (mcp-client-name, mcp-client-version) on every connection. Tools not in allowedTools excluded from listTools sent to the model. Tool-call errors forwarded as is_error:true instead of throwing. + +6. CRDT service CryptoUtil import [INTERNAL REFACTOR — no runtime change] +api/crdt/crdt.service.ts imports CryptoUtil directly from core/crypto/crypto-util instead of the shared barrel, completing the CryptoUtil migration off shared/utils/. crypto-util.ts no longer exported from the shared barrel. + +========================================================= +BUG FIXES +========================================================= +1. Email digest project-card count non-determinism [Velt-hosted digest feature; touches /v2/memory/ask usage] +Fixed by (1) running the structured /v2/memory/ask call at temperature:0 (ASK_TEMPERATURE), (2) anchoring the recencyDays window to UTC-day boundaries ([today − recencyDays, today)) rather than Date.now(), and (3) rewriting the grounding gate in SummaryService.fetchActionItems to key only on deterministic signals (recordsSearched >= 1 AND parsedBullets.length >= 1), removing the confidence float entirely. OI-010 closed: confidence threshold removed, not re-tuned. Impact: two digest runs within the same UTC day over identical Firestore data now produce identical topProjects.length and card names. confidence logged for diagnostics only. streamAskNLQuery retains STREAM_ASK_TEMPERATURE=1 (unchanged). + +2. validateInstructionsForStrategy [DEVELOPER-VISIBLE validation] +Fixed CustomAgentConfigValidator.validateInstructionsForStrategy to correctly identify strategies that require instructions (ai, service+ai, stagehand-agent) using a STRATEGIES_REQUIRING_INSTRUCTIONS set. Agent create/update for strategies that require instructions now correctly return 400 INVALID_ARGUMENT when instructions are missing/empty. + +3. listVersions secret redaction (fail-closed) [DEVELOPER-VISIBLE security] +Fixed AgentsServiceV2.listVersions to apply AgentApiService.redactAgentSecretsForApi to every version snapshot before returning. If redaction throws, the call rethrows (fail-closed) rather than returning unredacted ciphertext (self-verify finding F1, AC-004). No { __enc } wrapper survives the listVersions read path; encrypted fields replaced with redacted placeholders ("__redacted__"). diff --git a/ai/agent-comments.mdx b/ai/agent-comments.mdx index fe4d2535d..814b087af 100644 --- a/ai/agent-comments.mdx +++ b/ai/agent-comments.mdx @@ -19,7 +19,6 @@ Any agent that can make an HTTP request can do this — a custom agent you regis 3. **The finding renders in Velt** as a suggestion that humans can review, accept, or reject. 4. **You read agent annotations back** with the Get Comment Annotations API using agent-specific filters (`agentId`, `executionId`, `agentSource`, and more). - ## The `agent` block Attach an `agent` object to the root comment (`commentData[0]`). It is discriminated on `agentSource`: diff --git a/ai/approval-engine/customize-behavior.mdx b/ai/approval-engine/customize-behavior.mdx index 2eaa3318e..9d626881f 100644 --- a/ai/approval-engine/customize-behavior.mdx +++ b/ai/approval-engine/customize-behavior.mdx @@ -9,6 +9,8 @@ This is the deep-dive reference. Setup covers the happy path. This page covers e Every node has a `nodeId`, a `type` (`agent`, `human`, or `webhook`), a `config` block, and an optional `slaMs` deadline. `webhook` nodes validate in a definition but their runtime handler isn't enabled in v1, so this section covers the two runnable types. (The deferred `webhook` node is distinct from the [inbound webhook handler](/ai/approval-engine/overview#inbound-webhook-handler) and from [per-execution webhook delivery](/ai/approval-engine/setup#step-4-get-the-outcome), both of which are live in v1.) +Every node also accepts optional cosmetic `name` (1–200 chars) and `description` (≤ 2000 chars) fields, echoed verbatim in `NodeView`. They're labels for visual graph editors and don't affect runtime behavior. + ### Agent nodes | Field | Type | Notes | @@ -56,86 +58,52 @@ Exactly one of `reviewers[]` (preferred) or `reviewerIds[]` (legacy) must be pro } ``` -### Per-human-node `onReject` shorthand - -Authors can express the rejection path directly on a `human` node instead of declaring a top-level `loops[]` region or hand-writing an edge with `when: decision == 'reject'`. Two mutually-exclusive forms: +### Rejection paths are edges -**Form A — route on reject (synthesizes an edge):** +A `human` node carries no rejection config. Its reject path is an outgoing `on:"reject"` edge — see [the edge model](#edge-model) below. Every `human` node must have one, or the definition is rejected with `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH`. -```json theme={null} -{ - "nodeId": "human-review", - "type": "human", - "config": { - "reviewers": [{ "userId": "u1", "mandatory": true }], - "onReject": { "routeToNodeId": "human-escalate" } - } -} -``` +## Edge model -**Form B — loop back on reject (synthesizes a top-level loop region):** +All workflow transitions — approve routing, reject routing, group fan-out/in, and loop-backs — are expressed as a single unified `edges[]` array. Each edge is `{ from, to, on?, when?, loop? }`. ```json theme={null} -{ - "nodeId": "human-review", - "type": "human", - "config": { - "reviewers": [{ "userId": "u1", "mandatory": true }], - "onReject": { - "loopBack": { - "toNodeId": "agent-draft", - "maxIterations": 3, - "onExhausted": { "routeToNodeId": "human-final-call" } - } - } - } -} +{ "from": "human-review", "to": "rework-notice", "on": "reject" } ``` -| Field | Type | Required | Description | -| --------------------------------------------- | ----------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `onReject.routeToNodeId` | string | one of | Synthesizes an edge `{ from: , to: routeToNodeId, when: 'output.decision == \'reject\'' }`. | -| `onReject.loopBack.toNodeId` | string | one of | Synthesizes a top-level [loop region](#loop-regions) with `entryNodeId = toNodeId`. Body inferred from the topological closure (forward-reachable from `toNodeId` ∩ backward-reachable to the rejecting node). | -| `onReject.loopBack.maxIterations` | integer | no | Default 5. Range 1–20. | -| `onReject.loopBack.onExhausted.routeToNodeId` | string | no | Same semantics as `LoopRegionDef.onExhausted.routeToNodeId`. | -| `onReject.loopBack.when` | string (JSON-AST) | no | Custom predicate. Defaults to mandatory-reject. | - -### Strict-mode validation +| Field | Type | Required | Notes | +| ------ | --------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `from` | `EdgeEndpoint` | yes | Bare node-id string, `{ kind: "node", nodeId }`, or `{ kind: "group", groupId }`. | +| `to` | `EdgeEndpoint` | yes | Same shape as `from`. | +| `on` | enum | no (default `"always"`) | `approve` / `reject` / `always` / `exhausted` / `custom`. `approve` and `reject` auto-compile their predicates. | +| `when` | JSON-AST string | only when `on:"custom"` | Custom predicate over the source step's output. Invalid on any other `on` role. | +| `loop` | `{ maxIterations }` | only on an `on:"reject"` back-edge | `maxIterations` 1–20. Marks the reject edge as a loop-back; the server derives the loop region. | -Every `human` node MUST satisfy ONE of: +### Reject, loop-back, and exhausted -1. `config.onReject` is set (any form), OR -2. The node is a body member of a top-level `loops[]` entry. +- **Reject path:** an outgoing `on:"reject"` edge from the rejecting node. -Otherwise the API returns `INVALID_ARGUMENT` with message `Human nodes missing a reject path: `. This forces explicit rejection routing — silent dead-ends on rejection are a bug. + ```json theme={null} + { "from": "human-review", "to": "rework-notice", "on": "reject" } + ``` -### What desugaring rejects +- **Loop-back:** an `on:"reject"` edge whose `to` is an ancestor of `from`, marked with `loop`. The server derives the loop region (entry node, body, cap) from the marked edge. -The `onReject` shorthand is converted to canonical `edges[]` + `loops[]` at definition write time. The pass returns `INVALID_ARGUMENT` for: + ```json theme={null} + { "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } + ``` -| Condition | Why | -| ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `routeToNodeId` (or `loopBack.toNodeId` / `loopBack.onExhausted.routeToNodeId`) references a node that doesn't exist. | Target must be declared. | -| The synthesized reject edge would duplicate an existing user-supplied edge. | Pick one — declare the edge yourself or use the shorthand. | -| The rejecting node has an unconditional outgoing edge alongside `routeToNodeId`. | The unconditional edge would also fire on rejection. Add an explicit `when` (e.g. `decision == 'approve'`) to the forward edge, or switch to `loopBack`. | -| `loopBack` body-inference returns empty (no path from `toNodeId` to this node along `edges[]`). | Customer must add edges or declare a top-level `loops[]` region. | -| The rejecting node is already a body member of another top-level loop, OR the inferred body would overlap another loop's body. | A node may belong to at most one loop. Restructure or remove the conflicting loop. | -| `loopBack.toNodeId` equals the node itself (degenerate self-loop). | No valid body can be inferred. | +- **Exhausted handling:** a sibling `on:"exhausted"` edge from the same `from`, fired when the loop's `maxIterations` cap is reached. Without it, an exhausted loop rolls the execution up to `failed`. -### `onReject.routeToNodeId` on a `joinOnQuorum` group member is dead code + ```json theme={null} + { "from": "human-review", "to": "escalate", "on": "exhausted" } + ``` -The shorthand satisfies the strict-mode reject-path rule, but at runtime `joinOnQuorum` suppresses per-member fan-out — so the synthesized reject-gated edge can never fire. If you want per-rejecter routing on parallel reviewers, use `cancelOnQuorum` or `waitAll` (per-member fan-out preserved), or wrap the group in a top-level `loops[]` body so the loop interceptor handles rejections. +### Custom predicates -### Canonical form after desugaring - -The stored definition has `onReject` STRIPPED from human node configs and the synthesized edges/loops appended to the top-level arrays. `GET /definitions/get` returns the canonical form (auto-synthesized loops carry a `loopId` prefix `auto__loopback`). If you want to preserve your original input, store it on your side. - -## Edge gating expressions - -Edges can carry an optional `when` expression evaluated against the source step's output. Expressions compile at write time (pure AST, no `eval`) and walk at runtime. No untrusted code ever runs. +Use `on:"custom"` with a `when` expression to gate an edge on the source step's output. `when` is a JSON-AST string — the engine parses it as JSON and evaluates it with a safe walker, never as JavaScript. No untrusted code ever runs. `when` is invalid on any non-custom edge. ```json theme={null} -{ "from": "brand-check", "to": "legal-review", "when": "output.passesBrandCheck == true" } +{ "from": "brand-check", "to": "legal-review", "on": "custom", "when": "{\"op\":\"eq\",\"args\":[{\"var\":\"output.passesBrandCheck\"},true]}" } ``` **Supported operators:** equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`. @@ -148,7 +116,19 @@ Edges can carry an optional `when` expression evaluated against the source step' | `step.*` | The source step's metadata (status, timing). | | `execution.input.*` | The `triggerContext` you passed on dispatch. | -If `when` is omitted, the edge always fires. +An `on:"always"` edge (the default when `on` is omitted) always fires. + +### Groups as edge sources + +A group can be an edge source (`from: { kind: "group", groupId }`), giving it one collective branch instead of N per-member fan-outs: + +- **`waitAll` edge-source:** all members must terminate, then the group takes one collective branch by **unanimity** — `approve` if every member approved, else `reject`. Provide both an `on:"approve"` and an `on:"reject"` branch; exactly one fires. Successor step ID: `group___to__`. A routed collective reject is *not* a failed run — when the reject branch's successor completes, the execution rolls up to `completed`, and a member whose rejection is handled by the group edge is surfaced as `completed` with `output.decision="reject"`, never `failed`. +- **`cancelOnQuorum` edge-source:** fires one collective approve-successor on approval-**quorum** (same as `joinOnQuorum`). A forward `on:"reject"` from a `joinOnQuorum` or `cancelOnQuorum` group is rejected as a dead edge — those policies fan out only on approval-quorum. +- **Edge-to-group fan-out** (`to: { kind: "group", groupId }`) expands to one compiled edge per group member for all quorum policies. + +### The compiled view + +Every `DefinitionView` returns a read-only `compiled` block alongside the authored `edges` (which echo `sourceEdges` byte-for-byte): `compiled.forwardEdges` is the runtime forward-edge list (group endpoints expanded, `on` roles compiled to predicate ASTs) and `compiled.loops` is the server-derived loop region list. See [Get Definition](/api-reference/rest-apis/v2/approval-engine/definitions/get-definition#the-compiled-block) for the field schema. ## SLA and breach handling @@ -192,7 +172,7 @@ Two consequences: | Policy | Side effect on first-time approval-quorum-met | Per-member fan-out | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `waitAll` (default) | Emits `group.quorum-met` event only. Group is purely informational. | Each member's outgoing edges fire on its own completion. If two members both fan out to the same downstream node, you get two downstream step instances. | +| `waitAll` (default) | Without group-source edges, emits `group.quorum-met` event only and is informational. When used as an edge source, waits for all members and fires one collective approve/reject branch by unanimity. | Each member's outgoing edges fire on its own completion. If two members both fan out to the same downstream node, you get two downstream step instances. | | `cancelOnQuorum` | Emits `group.quorum-met` AND cancels every sibling member step still in `waiting` (system-actor cancellation, audit reason `group-quorum-met`). | Each completing member still fans out per-edge. Cancelled siblings do not fan out. | | `joinOnQuorum` | Emits `group.quorum-met`, cancels waiting siblings, AND fires a single group-owned downstream fan-out: one new step per shared outgoing-edge target with deterministic stepId `group___to__`. The successor's input is `{ groupOutputs, groupId, quorum, totalApproved }`. | Suppressed for group members. The group container owns fan-out, so downstream successors run exactly once. | @@ -219,48 +199,34 @@ In the example, `brand` alone approving doesn't satisfy quorum even if `quorum: ## Loop regions -A loop region lets a workflow re-enter an earlier node when a reviewer rejects, instead of failing outright. Declare loops at the top level of a definition, peer to `groups[]`: +A loop region lets a workflow re-enter an earlier node when a reviewer rejects, instead of failing outright. You don't declare loops directly — you mark an `on:"reject"` edge with `loop` (its `to` must be an ancestor of `from`), and the server derives the loop region (entry node, body, cap) on the [compiled graph](#the-compiled-view). Add a sibling `on:"exhausted"` edge to route when the cap is reached. ```json theme={null} -{ - "loops": [ - { - "loopId": "draft-revision", - "entryNodeId": "agent-draft", - "bodyNodeIds": ["agent-draft", "human-legal", "human-brand"], - "onIterationReject": { - "when": "{\"op\":\"and\",\"args\":[{\"op\":\"eq\",\"args\":[{\"var\":\"output.decision\"},\"reject\"]},{\"op\":\"eq\",\"args\":[{\"var\":\"output.rejectorMandatory\"},true]}]}" - }, - "onExhausted": { "routeToNodeId": "human-escalate" }, - "maxIterations": 5 - } - ] -} +{ "from": "human-legal", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 5 } } +{ "from": "human-legal", "to": "human-escalate", "on": "exhausted" } ``` -| Field | Type | Required | Description | -| --------------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `loopId` | string | yes | 1–64 chars. Stable identifier for this loop. | -| `entryNodeId` | string | yes | Node spawned first on each iteration. Must be a member of `bodyNodeIds`. | -| `bodyNodeIds` | string[] | yes | 1–50 nodes inside the loop's iteration scope. May include parallel-group members. | -| `onIterationReject.when` | string (JSON-AST) | no | JSON-AST predicate evaluated when the loop body's iteration-terminal step finishes. If it matches, iteration N+1 spawns. Default: `decision == 'reject' && rejectorMandatory == true`. | -| `onExhausted.routeToNodeId` | string | no | Node spawned when `maxIterations` is reached. Receives `previousAttempts` like a regular iteration entry. If omitted, the execution rolls up to `failed`. | -| `maxIterations` | integer | yes | 1–20. Hard cap per execution. | +The derived region is surfaced read-only as `compiled.loops[]`: + +| Field | Type | Description | +| --------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `loopId` | string | Server-assigned stable identifier for the derived loop. | +| `entryNodeId` | string | Node re-entered first on each iteration (the reject edge's `to`). | +| `bodyNodeIds` | string[] | Nodes inside the loop's iteration scope, derived from the topological closure between `to` and `from`. | +| `maxIterations` | integer | 1–20. Hard cap per execution, taken from the reject edge's `loop.maxIterations`. | +| `onExhausted` | `{ routeToNodeId } \| null` | Target of the sibling `on:"exhausted"` edge. `null` rolls the execution up to `failed` at the cap. | + +The default iteration predicate is `decision == 'reject' && rejectorMandatory == true`. The unified contract does not support custom loop predicates on loop-back edges: `loop` is valid only on an `on:"reject"` back-edge. ### Body-shape constraint -The body must be one of: +The derived loop body must be one of: 1. **Single-terminal sequential.** Exactly one body node has outgoing edges that leave the body — that node is the iteration-terminal. 2. **Group-bounded.** The set of exit-bearing body nodes equals the `memberNodeIds` of one parallel group with `onQuorumMet: 'joinOnQuorum'`, every member lies inside the body, and the group has `quorum === expectedSteps`. The linter rejects other shapes with `loop-body-must-have-single-terminal`. -### When to use a loop vs. the `onReject` shorthand - -- **Use a loop region** when multiple parallel reviewers should share a single retry counter, when the retry zone needs to contain a `joinOnQuorum` group as a unit, or when you want explicit control over the `loopId`. -- **Use the [`onReject` shorthand](#per-human-node-onreject-shorthand) on a single human node** for simple per-node reject routing (route to a different node on reject, or kick back to a single earlier node). - ### `previousAttempts` payload threaded into iteration N+1 The entry step of iteration N+1 receives: @@ -295,7 +261,7 @@ Two new event types fire alongside the standard `step.*` and `execution.*` event ## Linter rules -Definitions are linted at create and update time. Any rule violation is rejected with `INVALID_ARGUMENT` and an explicit code in the error message. +Definitions are validated at create and update time. Authored-edge provenance errors come from `compileGraph`; graph-shape errors come from the linter. Any violation is rejected with `INVALID_ARGUMENT` and an explicit code in the error message. | Code | Meaning | | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | @@ -315,15 +281,25 @@ Definitions are linted at create and update time. Any rule violation is rejected | `group-required-not-in-members` | An entry in `requiredNodeIds` is not in `memberNodeIds`. | | `group-required-exceeds-quorum` | `requiredNodeIds.length > quorum`. | | `group-node-in-multiple-groups` | A node appears as a member of two or more groups. | -| `loop-duplicate-id` | Two loops share the same `loopId`. | -| `loop-entry-must-be-in-body` | `entryNodeId` is not listed in `bodyNodeIds`. | -| `loop-body-member-missing` | A `bodyNodeIds` entry isn't a declared node. | -| `loop-body-unreachable-from-entry` | Some body node is unreachable from `entryNodeId` along body-internal edges. | -| `loop-body-must-have-single-terminal` | Body shape is neither single-terminal sequential nor group-bounded — see [Loop regions › Body-shape constraint](#body-shape-constraint). | -| `loop-node-in-multiple-loops` | A node appears in more than one loop body. A node may belong to at most one loop. | -| `loop-on-exhausted-route-to-not-found` | `onExhausted.routeToNodeId` references an unknown node. | -| `loop-on-exhausted-route-to-in-body` | `onExhausted.routeToNodeId` is itself a body node — exhausted-exit must escape the loop body. | -| `loop-group-bounded-quorum-must-equal-expected` | Body is group-bounded but the bounding `joinOnQuorum` group has `quorum < expectedSteps`. Force `quorum === expectedSteps` so iteration-terminal coincides with all-members-done; otherwise a late rejection races against an already-fired join successor. | +| `loop-body-must-have-single-terminal` | Derived loop body shape is neither single-terminal sequential nor group-bounded — see [Loop regions › Body-shape constraint](#body-shape-constraint). | +| `loop-group-bounded-quorum-must-equal-expected` | Derived body is group-bounded but the bounding `joinOnQuorum` group has `quorum < expectedSteps`. Force `quorum === expectedSteps` so iteration-terminal coincides with all-members-done; otherwise a late rejection races against an already-fired join successor. | + +### Edge model validation errors + +The unified `edges[]` contract is validated at create and update time. Violations are rejected with `INVALID_ARGUMENT` and one of these keys in the error message: + +| Error key | Cause | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN` | `on:"custom"` without a non-empty `when`. | +| `APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM` | `when` supplied on a non-custom edge. | +| `APPROVAL_EDGE_LOOP_REQUIRES_REJECT` | `loop` on a non-reject edge. | +| `APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR` | `reject`+`loop` whose `to` is not an ancestor of `from`. | +| `APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP` | `on:"reject"` to an ancestor without `loop` (unmarked cycle). | +| `APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING` | `on:"exhausted"` with no sibling `reject`+`loop` from the same `from`. | +| `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` | Human node with no outgoing `on:"reject"` edge. | +| `APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN` | Both endpoints are group containers. | +| `APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP` | Forward `on:"reject"` from a `joinOnQuorum` / `cancelOnQuorum` group (dead edge — those policies fan out only on approval-quorum). | +| `APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM`| `reject`+`loop` back-edge from a non-`joinOnQuorum` group. | ## Events @@ -389,7 +365,7 @@ All errors follow the standard envelope: | Code | Meaning | Typical cause | | --------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `INVALID_ARGUMENT` | Schema or linter failure. | Missing field, wrong type, value out of range, linter rule violation. | +| `INVALID_ARGUMENT` | Schema, `compileGraph`, or linter failure. | Missing field, wrong type, value out of range, edge contract violation, or linter rule violation. | | `UNAUTHENTICATED` | Missing or invalid `x-velt-auth-token`. | | | `PERMISSION_DENIED` | Auth token valid but lacks the required scope. | `/steps/resolve` with `reviewer-approve` / `reviewer-reject` and `actorId` not in the step's reviewer list. | | `NOT_FOUND` | Target doc does not exist. | Unknown `executionId`, `definitionId`, or `stepId`. | @@ -454,6 +430,7 @@ interface DefinitionView { nodes: NodeView[]; edges: EdgeView[]; groups: ParallelGroupDef[] | null; + compiled: CompiledGraph; triggers: WorkflowTriggerConfig[] | null; tags: string[] | null; custom: Record | null; @@ -462,6 +439,30 @@ interface DefinitionView { status: 'active' | 'tombstoned'; } +type JsonAst = Record; + +interface CompiledGraph { + forwardEdges: CompiledForwardEdge[]; + loops: CompiledLoopRegion[]; +} + +interface CompiledForwardEdge { + from: string; + to: string; + role: 'approve' | 'reject' | 'always' | 'exhausted' | 'custom'; + when: JsonAst | null; + fromGroupId?: string; + toGroupId?: string; +} + +interface CompiledLoopRegion { + loopId: string; + entryNodeId: string; + bodyNodeIds: string[]; + maxIterations: number; + onExhausted: { routeToNodeId: string } | null; +} + interface ApprovalEventView { eventId: string; seq: number; // monotonic per-execution diff --git a/ai/approval-engine/overview.mdx b/ai/approval-engine/overview.mdx index 05db712f5..1e2a03a18 100644 --- a/ai/approval-engine/overview.mdx +++ b/ai/approval-engine/overview.mdx @@ -82,11 +82,11 @@ Work units that can run. ### Edges -Connections between nodes. Edges optionally carry a `when` expression like `output.passesBrandCheck == true`. Expressions compile at write time (pure AST, no `eval`) and walk at runtime. +Connections between nodes. Every transition — approve routing, reject routing, group fan-out/in, and loop-backs — is a single unified `edges[]` entry. Each edge carries an `on` role (`approve` / `reject` / `always` / `exhausted` / `custom`); a `human` node's reject path is an outgoing `on:"reject"` edge. A reject back-edge marked with `loop` becomes a loop region. Use `on:"custom"` with a JSON-AST `when` string like `{"op":"eq","args":[{"var":"output.passesBrandCheck"},true]}` to gate on the source step's output. Expressions compile at write time (pure AST, no `eval`) and walk at runtime. -Supported operators: equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`. +Supported operators: equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`. Path roots: `output.*`, `step.*`, `execution.input.*`. -Path roots: `output.*`, `step.*`, `execution.input.*`. +A group can also be an edge source: a `waitAll` group fans out one collective approve/reject branch by unanimity, and a `cancelOnQuorum` group fans out a collective approve successor on quorum. Every `DefinitionView` returns a read-only `compiled` block (`compiled.forwardEdges` + `compiled.loops`) so clients can render the runtime graph without re-implementing the compiler. See [the edge model](/ai/approval-engine/customize-behavior#edge-model). ### Execution @@ -165,6 +165,6 @@ The inbound handler enforces: - Node configuration, edge gating expressions, parallel groups and quorum policies, SLAs, linter rules, event reference, and the error vocabulary. + Node configuration, the unified edge model, custom predicates, parallel groups and quorum policies, SLAs, linter rules, event reference, and the error vocabulary. \ No newline at end of file diff --git a/ai/approval-engine/setup.mdx b/ai/approval-engine/setup.mdx index 32d519f8e..d1fe0f9e0 100644 --- a/ai/approval-engine/setup.mdx +++ b/ai/approval-engine/setup.mdx @@ -52,16 +52,16 @@ You'll build the smallest workflow that exercises the whole engine: one human ap ```mermaid flowchart LR Start([Dispatch:
'review this item']) --> M["manager-approval
(human)"] - M -->|"when decision == approve"| Done([Run complete]) - M -->|"on reject"| RW["rework-notice
(agent)"] + M -->|"approve: no outgoing edge"| Done([Run complete]) + M -->|"on: reject"| RW["rework-notice
(agent)"] RW --> Done ``` ### Step 1: Create the definition -A `human` node must declare what happens on rejection: either an `onReject` shorthand or membership in a `loops[]` body. Here we use the `onReject.routeToNodeId` shorthand to send rejections to a follow-up node, and we gate the success edge with `when: decision == 'approve'` so it only fires on approval. +A `human` node must declare a reject path: an outgoing `on:"reject"` edge. Here we send rejections to a follow-up node with an `on:"reject"` edge. Approvals have no outgoing edge in this minimal workflow, so the run completes after the approval step. The `approve` and `reject` roles auto-compile their predicates, so you don't hand-write a `when` for them. -The `when` value is a **JSON-AST string**: the engine parses it as JSON and evaluates it with a safe walker, never as JavaScript. The follow-up node uses the reserved `__mock__` agent id so you can run this end-to-end without registering a real agent (use a real `agentId` in production). +The follow-up node uses the reserved `__mock__` agent id so you can run this end-to-end without registering a real agent (use a real `agentId` in production). ```bash curl -X POST https://api.velt.dev/v2/workflow/definitions/create \ @@ -79,8 +79,7 @@ curl -X POST https://api.velt.dev/v2/workflow/definitions/create \ "type": "human", "config": { "reviewers": [{ "userId": "u_manager_01", "mandatory": true }], - "commentBody": "Please review and approve this document.", - "onReject": { "routeToNodeId": "rework-notice" } + "commentBody": "Please review and approve this document." } }, { @@ -90,19 +89,15 @@ curl -X POST https://api.velt.dev/v2/workflow/definitions/create \ } ], "edges": [ - { - "from": "manager-approval", - "to": "rework-notice", - "when": "{\"op\":\"eq\",\"args\":[{\"var\":\"output.decision\"},\"reject\"]}" - } + { "from": "manager-approval", "to": "rework-notice", "on": "reject" } ] } }' ``` -A successful create returns a `DefinitionView` with `version: 1` and `status: "active"`. Definitions are linted at write time: cycles, dangling edges, unreachable nodes, and quorum misconfiguration all fail before you ever dispatch. If the engine rejects the definition you'll get `INVALID_ARGUMENT` with a [linter code](/ai/approval-engine/customize-behavior#linter-rules) in the message. +A successful create returns a `DefinitionView` with `version: 1` and `status: "active"`. Definitions are validated at write time: schema errors, authored-edge contract errors from `compileGraph`, cycles, dangling edges, unreachable nodes, and quorum misconfiguration all fail before you ever dispatch. If the engine rejects the definition you'll get `INVALID_ARGUMENT` with a validation key in the message. A `human` node with no outgoing `on:"reject"` edge is rejected with `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH`. -The `onReject.routeToNodeId` shorthand synthesizes the reject-gated edge for you, so you only need to author the explicit approve-side routing. See [Per-human-node `onReject` shorthand](/ai/approval-engine/customize-behavior#per-human-node-onreject-shorthand) for choosing a rejection strategy. Full request shape: [Create Definition](/api-reference/rest-apis/v2/approval-engine/definitions/create-definition). +See [the edge model](/ai/approval-engine/customize-behavior#edge-model) for reject, loop-back, and custom routing. Full request shape: [Create Definition](/api-reference/rest-apis/v2/approval-engine/definitions/create-definition). ### Step 2: Dispatch an execution @@ -318,9 +313,9 @@ Once the basics click, you compose richer graphs. Here's an AI-assisted parallel Short pointers to the features you'll reach for next. Each links to the decision-level guidance and the field-level contract. -- **Conditional routing (`when` expressions).** Gate any edge with a `when` JSON-AST predicate over `output.*`, `step.*`, and `execution.input.*`. Operators include equality, comparison, boolean and/or/not, regex, includes, startsWith, endsWith, length, isEmpty. → [Edge gating expressions](/ai/approval-engine/customize-behavior#edge-gating-expressions). -- **Parallel groups & quorum.** Declare a `groups[]` entry to run reviewers in parallel under one of three policies: `waitAll` (observability only), `cancelOnQuorum` (stop bothering siblings once enough approve), `joinOnQuorum` (run the successor once after quorum). Use `requiredNodeIds` for "these specific people must approve." → [`onQuorumMet` policies](/ai/approval-engine/customize-behavior#onquorummet-policies) · [Specific-must-approve quorum](/ai/approval-engine/customize-behavior#specific-must-approve-quorum). -- **Rejection handling: shorthand vs. loops.** For one reviewer, `onReject.routeToNodeId` (route away) or `onReject.loopBack` (retry up to N times, then escalate) usually suffice. When multiple parallel reviewers must share one retry budget, or a whole stage must rewind as a unit, declare a top-level `loops[]` region instead. → [Loop regions](/ai/approval-engine/customize-behavior#loop-regions) · [When to use a loop vs. the `onReject` shorthand](/ai/approval-engine/customize-behavior#when-to-use-a-loop-vs-the-onreject-shorthand). +- **Conditional routing (`when` expressions).** Gate any edge with `on:"custom"` and a `when` JSON-AST predicate over `output.*`, `step.*`, and `execution.input.*`. Operators include equality, comparison, boolean and/or/not, regex, includes, startsWith, endsWith, length, isEmpty. → [Custom predicates](/ai/approval-engine/customize-behavior#custom-predicates). +- **Parallel groups & quorum.** Declare a `groups[]` entry to run reviewers in parallel under one of three policies: `waitAll` (observability only, or a collective unanimity branch when used as an edge source), `cancelOnQuorum` (stop bothering siblings once enough approve), `joinOnQuorum` (run the successor once after quorum). Use `requiredNodeIds` for "these specific people must approve." → [`onQuorumMet` policies](/ai/approval-engine/customize-behavior#onquorummet-policies) · [Specific-must-approve quorum](/ai/approval-engine/customize-behavior#specific-must-approve-quorum). +- **Rejection handling: reject edges & loop-backs.** A human node's reject path is an outgoing `on:"reject"` edge. To retry up to N times, mark a reject back-edge to an earlier node with `loop { maxIterations }` and add a sibling `on:"exhausted"` edge to escalate at the cap; the server derives the loop region. → [The edge model](/ai/approval-engine/customize-behavior#edge-model) · [Loop regions](/ai/approval-engine/customize-behavior#loop-regions). - **SLA timers & escalation.** Set `slaMs` on a node to enforce a deadline; on breach the step becomes `breached` and the engine follows your outgoing edges. **Gotcha:** a node with `slaMs` must have an edge that routes on `status == 'breached'`, or the definition is rejected (`missing-breach-edge`). Agent nodes also have a hard runtime ceiling (`agentMaxRuntimeMs`, default 30 min). → [SLA and breach handling](/ai/approval-engine/customize-behavior#sla-and-breach-handling). - **Agent nodes.** An agent node requires `agentId` and `urlPath` (a dot-path into `triggerContext` that resolves the URL the agent should act on). The step output exposes `agentExecutionStatus`, `agentResultsSummary`, and a `decision` (`approve` when the agent passed). Some configurations post findings that are resolved via [Record Agent Resolution](/api-reference/rest-apis/v2/approval-engine/steps/record-agent-resolution). → [Agent nodes](/ai/approval-engine/customize-behavior#agent-nodes). - **Externally-triggered runs & async callbacks.** Beyond dispatching from your backend, external systems can kick off runs or complete long-running steps via the inbound webhook surface. → [Inbound webhook handler](/ai/approval-engine/overview#inbound-webhook-handler). @@ -352,7 +347,7 @@ Errors use the standard envelope with a gRPC-style status code: | Code | Meaning | | --- | --- | -| `INVALID_ARGUMENT` | Request failed schema or linter validation. | +| `INVALID_ARGUMENT` | Request failed schema, `compileGraph`, or linter validation. | | `UNAUTHENTICATED` | Missing/invalid `x-velt-auth-token`. | | `PERMISSION_DENIED` | Token valid but lacks the required scope (e.g. admin-only `/steps/resolve`). | | `NOT_FOUND` | Unknown `executionId`, `definitionId`, or `stepId`. | @@ -362,12 +357,12 @@ Errors use the standard envelope with a gRPC-style status code: Most common "the engine rejected my definition" causes (all `INVALID_ARGUMENT`, with a code in the message): -- **Human node missing a reject path:** add `onReject`, or include the node in a `loops[]` body. +- **`APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH`:** a human node has no outgoing `on:"reject"` edge. Add one. - **`missing-breach-edge`:** a node has `slaMs` but no edge routes on `status == 'breached'`. - **`when` written as JavaScript:** it must be a JSON-AST string, not `"output.decision == 'approve'"`. - **Group quorum > `expectedSteps`**, or a non-blocking agent placed in a quorum group (it has no `decision`, so quorum can never be met). -See the full list under [Linter rules](/ai/approval-engine/customize-behavior#linter-rules) and [Canonical codes](/ai/approval-engine/customize-behavior#canonical-codes). +See the full list under [Linter rules](/ai/approval-engine/customize-behavior#linter-rules), [Edge model validation errors](/ai/approval-engine/customize-behavior#edge-model-validation-errors), and [Canonical codes](/ai/approval-engine/customize-behavior#canonical-codes). ## Limitations in v1 diff --git a/ai/custom-agents.mdx b/ai/custom-agents.mdx new file mode 100644 index 000000000..121e4cb5e --- /dev/null +++ b/ai/custom-agents.mdx @@ -0,0 +1,105 @@ +--- +title: "Custom Agents" +description: "Configure custom agent context gathering, tool execution, built-in agents, and configuration safety rules." +--- + +Custom agents can gather external context before they run and can use tool-calling execution loops when a single LLM call is not enough. + +## REST API context gathering + +Use the `rest-api` context-gathering strategy when your agent needs live data from your own APIs before it executes, such as user profiles, CRM records, entitlement state, or billing status. Velt fetches the configured endpoints at execution time and injects the collected results into the prompt with the `{{restApiData}}` template variable. + +```json +{ + "contextGathering": { + "strategies": ["web-page-text", "rest-api"], + "strategyOptions": { + "rest-api": { + "endpoints": [ + { + "url": "https://api.example.com/customers/{{userContext.customerId}}", + "description": "Customer profile and subscription state", + "method": "GET", + "auth": { + "type": "header", + "headers": { "X-Api-Key": "YOUR_API_KEY" } + }, + "cacheTtlSeconds": 300, + "timeoutMs": 10000, + "responsePath": "data.customer" + } + ], + "maxResponseBytes": 1000000 + } + } + } +} +``` + +### `strategyOptions["rest-api"]` + +| Field | Type | Required | Notes | +| ------------------ | ------------------- | -------- | ------------------------------------------------------------ | +| `endpoints` | `RestApiEndpoint[]` | yes | 1–10 endpoints. | +| `maxResponseBytes` | number | no | Per-endpoint cap. Default `1000000`, max `5000000`. | + +### `RestApiEndpoint` + +| Field | Type | Required | Notes | +| ----------------- | ----------------------------------------- | -------- | --------------------------------------------------------------------- | +| `url` | string | yes | SSRF-validated URL. Supports `{{variable}}` templating. | +| `description` | string | yes | Surfaced in the prompt alongside the URL and response. | +| `method` | `"GET"` / `"POST"` / `"PUT"` / `"PATCH"` / `"DELETE"` | no | Default `"GET"`. | +| `auth` | `RestApiAuthConfig` | no | `none`, `bearer`, `basic`, or `header`. Secret fields are encrypted at rest and redacted on read. | +| `headers` | `Record` | no | Request headers. Supports templating. | +| `query` | `Record` | no | Query parameters. Supports templating. | +| `body` | unknown | no | Request body. Supports templating. | +| `cacheTtlSeconds` | number | no | In-process cache TTL per Cloud Function instance. Default `0`, max `86400`. | +| `timeoutMs` | number | no | Per-request timeout. Default `10000`, max `30000`. | +| `responsePath` | string | no | Dot-path to extract a sub-field of the JSON response. | + +Templating is supported in `url`, `headers`, `query`, and `body` with `{{variable}}`, `{{userContext.X}}`, and `{{variables.X}}`. Endpoint URLs are SSRF-guarded and block internal, loopback, and link-local hosts. Secret auth fields are encrypted at rest and redacted on every read path. + +## MCP tool execution + +Use `execution.executionStrategy: "mcp-tools"` when the agent needs a multi-turn tool loop. The agent connects to one or more customer-configured MCP servers over remote HTTP / Streamable HTTP transport, sends only the allowed tool schemas to the model, executes model-requested tools, and returns a structured response. + +```json +{ + "execution": { + "executionStrategy": "mcp-tools", + "mcpServers": [ + { + "url": "https://docs.example.com/mcp", + "auth": { + "type": "header", + "headers": { "X-Api-Key": "YOUR_API_KEY" } + }, + "allowedTools": ["search_docs", "fetch_page"] + } + ] + } +} +``` + +MCP auth headers are encrypted at rest and redacted on read. Per-server tool allowlists, per-turn redaction, and a max-turn cap prevent runaway loops. Tool-call errors are forwarded back to the model as tool errors. Claude and Gemini provider adapters support the tool loop; providers that do not support tools fail with `AI_TOOLS_NOT_SUPPORTED`. + +## Built-in docs code comparison agent + +The built-in `docs-code-comparison` agent verifies code snippets on a landing page against your docs site and reports mismatches as annotations. It uses `web-page-text` context gathering with `includeCodeBlocks: true` and the `mcp-tools` execution strategy against a remote docs MCP server. It works with Claude and Gemini. + +## Configuration safety + +Partial custom-agent config updates deep-merge nested objects, so updating one field under `execution` or `contextGathering` preserves sibling fields such as `execution.mcpServers`. + +Create and update-version payloads reject server-managed keys with `400 INVALID_ARGUMENT`: + +- `managedBy` +- `metadata.type` +- `metadata.category` +- `metadata.internal` +- `metadata.apiKey` + +A defense-in-depth strip also runs at the Firestore write layer before data is persisted. Agent config reads and version snapshots redact encrypted auth ciphertext before returning data to clients. If redaction fails, the request fails closed instead of returning unredacted secrets. + +Strategies that require instructions (`ai`, `service+ai`, and `stagehand-agent`) reject create and update payloads with `400 INVALID_ARGUMENT` when `instructions` is missing or empty. diff --git a/api-reference/rest-apis/v2/approval-engine/definitions/create-definition.mdx b/api-reference/rest-apis/v2/approval-engine/definitions/create-definition.mdx index 8cfd0f0df..59c9c3f30 100644 --- a/api-reference/rest-apis/v2/approval-engine/definitions/create-definition.mdx +++ b/api-reference/rest-apis/v2/approval-engine/definitions/create-definition.mdx @@ -3,7 +3,7 @@ title: "Create Definition" api: "POST https://api.velt.dev/v2/workflow/definitions/create" --- -Use this API to register a new workflow definition (the static blueprint of nodes, edges, and optional parallel groups). Definitions are linted at write time — invalid graphs are rejected with an explicit linter code. +Use this API to register a new workflow definition (the static blueprint of nodes, edges, and optional parallel groups). Definitions are validated at write time — schema errors, `compileGraph` edge-contract errors, and linter graph-shape errors are rejected with explicit validation keys. # Endpoint @@ -48,19 +48,27 @@ Use this API to register a new workflow definition (the static blueprint of node
- 1–100 nodes. Each node has `nodeId`, `type` (`agent` / `human` / `webhook` — `webhook` validates but its runtime is deferred in v1), and a `config` block. See node configuration details in [Customize Behavior](/ai/approval-engine/customize-behavior#node-configuration). Each node also accepts an optional `slaMs` (integer, ms) — SLA deadline for the step. + 1–100 nodes. Each node has `nodeId`, `type` (`agent` / `human` / `webhook` — `webhook` validates but its runtime is deferred in v1), and a `config` block. See node configuration details in [Customize Behavior](/ai/approval-engine/customize-behavior#node-configuration). Each node also accepts an optional `slaMs` (integer, ms) — SLA deadline for the step — and optional cosmetic `name` (1–200 chars) and `description` (≤ 2000 chars) labels echoed verbatim in the response. - 0–500 edges. Each edge: `{ from, to, when? }`. The optional `when` expression is evaluated against the source step's output — see [edge gating expressions](/ai/approval-engine/customize-behavior#edge-gating-expressions). + 0–500 edges. Each edge: `{ from, to, on?, when?, loop? }`. `from` / `to` are `EdgeEndpoint`s — a bare node-id string, `{ kind: "node", nodeId }`, or `{ kind: "group", groupId }`. + + | Field | Type | Required | Notes | + | ------ | --------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | + | `from` | `EdgeEndpoint` | yes | Source node or group. | + | `to` | `EdgeEndpoint` | yes | Target node or group. | + | `on` | enum | no (default `"always"`) | `approve` / `reject` / `always` / `exhausted` / `custom`. `approve` and `reject` auto-compile their predicates. | + | `when` | JSON-AST string | only when `on:"custom"` | Custom predicate over the source step's output. Invalid on any other `on` role. See [custom predicates](/ai/approval-engine/customize-behavior#custom-predicates). | + | `loop` | `{ maxIterations }` | only on an `on:"reject"` back-edge | `maxIterations` 1–20. Marks the reject edge as a loop-back; the server derives the loop region. A sibling `on:"exhausted"` edge handles cap exhaustion. | + + A `human` node must have an outgoing `on:"reject"` edge or the definition is rejected with `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH`. See [the edge model](/ai/approval-engine/customize-behavior#edge-model) for the full role and loop semantics. 0–100 parallel-group definitions. See [parallel groups and quorum policies](/ai/approval-engine/customize-behavior#parallel-groups-and-quorum-policies). - - - 0–20 loop region declarations. A loop region lets a workflow re-enter an earlier node when a reviewer rejects, with previous-attempt context threaded forward and a hard cap on iterations. See [loop regions](/ai/approval-engine/customize-behavior#loop-regions) for the full schema, body-shape constraints, and linter rules. + A group can be an edge source (`from: { kind: "group", groupId }`): a `waitAll` group fans out a collective approve/reject decision by unanimity (every member approved → approve, else reject), and a `cancelOnQuorum` group fans out a collective approve successor on quorum. A forward `on:"reject"` from a `joinOnQuorum` / `cancelOnQuorum` group is a rejected dead edge. Edge-to-group fan-out (`to: { kind: "group", groupId }`) is accepted for all quorum policies. @@ -99,15 +107,21 @@ Use this API to register a new workflow definition (the static blueprint of node { "nodeId": "brand-check", "type": "agent", + "name": "Brand check", + "description": "Verifies copy against brand guidelines.", "config": { "agentId": "brand-agent-v1", "blocking": false, "requireNonEmptyOutput": true }, "slaMs": 3600000 } ``` -#### Edge object example +#### Edge object examples ```JSON -{ "from": "brand-check", "to": "legal-review", "when": "output.passesBrandCheck == true" } +{ "from": "brand-check", "to": "legal-review", "on": "approve" } +{ "from": "human-review", "to": "rework-notice", "on": "reject" } +{ "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } +{ "from": "human-review", "to": "escalate", "on": "exhausted" } +{ "from": "brand-check", "to": "legal-review", "on": "custom", "when": "{\"op\":\"eq\",\"args\":[{\"var\":\"output.passesBrandCheck\"},true]}" } ``` ## **Example Requests** @@ -121,24 +135,16 @@ Use this API to register a new workflow definition (the static blueprint of node "name": "Marketing copy approval", "scope": { "level": "apiKey" }, "nodes": [ - { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } }, - { "nodeId": "human-legal", "type": "human", "config": { "reviewers": [{ "userId": "u_legal_01", "mandatory": true }] } }, - { "nodeId": "human-brand", "type": "human", "config": { "reviewers": [{ "userId": "u_brand_01", "mandatory": true }] } }, - { "nodeId": "agent-publish", "type": "agent", "config": { "agentId": "publish-agent-v1" } } + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } }, + { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u_reviewer_01", "mandatory": true }] } }, + { "nodeId": "agent-publish", "type": "agent", "config": { "agentId": "publish-agent-v1" } }, + { "nodeId": "agent-rework", "type": "agent", "config": { "agentId": "rework-agent-v1" } } ], "edges": [ - { "from": "agent-draft", "to": "human-legal" }, - { "from": "agent-draft", "to": "human-brand" }, - { "from": "human-legal", "to": "agent-publish" }, - { "from": "human-brand", "to": "agent-publish" } - ], - "groups": [{ - "groupId": "parallel-review", - "memberNodeIds": ["human-legal", "human-brand"], - "expectedSteps": 2, - "quorum": 2, - "onQuorumMet": "joinOnQuorum" - }] + { "from": "agent-draft", "to": "human-review" }, + { "from": "human-review", "to": "agent-publish", "on": "approve" }, + { "from": "human-review", "to": "agent-rework", "on": "reject" } + ] } } ``` @@ -155,9 +161,26 @@ Use this API to register a new workflow definition (the static blueprint of node "description": null, "version": 1, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [ /* echoed back with resolved defaults */ ], - "edges": [ /* echoed back with when: null for unset gates */ ], - "groups": [ /* echoed back */ ], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } }, + { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u_reviewer_01", "mandatory": true }] } }, + { "nodeId": "agent-publish", "type": "agent", "config": { "agentId": "publish-agent-v1" } }, + { "nodeId": "agent-rework", "type": "agent", "config": { "agentId": "rework-agent-v1" } } + ], + "edges": [ + { "from": "agent-draft", "to": "human-review" }, + { "from": "human-review", "to": "agent-publish", "on": "approve" }, + { "from": "human-review", "to": "agent-rework", "on": "reject" } + ], + "groups": null, + "compiled": { + "forwardEdges": [ + { "from": "agent-draft", "to": "human-review", "role": "always", "when": null }, + { "from": "human-review", "to": "agent-publish", "role": "approve", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "approve"] } }, + { "from": "human-review", "to": "agent-rework", "role": "reject", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "reject"] } } + ], + "loops": [] + }, "triggers": null, "tags": null, "custom": null, @@ -168,6 +191,8 @@ Use this API to register a new workflow definition (the static blueprint of node } ``` +The read-only `compiled` block is added to every `DefinitionView` (create / get / list). `compiled.forwardEdges` is the runtime forward-edge list the engine drives execution from; `compiled.loops` is the derived loop region list. The authored `edges` still echo `sourceEdges` byte-for-byte. See [Get Definition](/api-reference/rest-apis/v2/approval-engine/definitions/get-definition) for the full `compiled` schema. + #### Failure Response ```JSON @@ -179,7 +204,7 @@ Use this API to register a new workflow definition (the static blueprint of node } ``` -**Errors:** `INVALID_ARGUMENT` (schema or linter failure; message includes the linter code) / `ALREADY_EXISTS` (definitionId already in use). +**Errors:** `INVALID_ARGUMENT` (schema, `compileGraph`, or linter failure; message includes the validation key) / `ALREADY_EXISTS` (definitionId already in use). ```js @@ -190,9 +215,12 @@ Use this API to register a new workflow definition (the static blueprint of node "description": null, "version": 1, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } } + ], "edges": [], - "groups": [], + "groups": null, + "compiled": { "forwardEdges": [], "loops": [] }, "triggers": null, "tags": null, "custom": null, diff --git a/api-reference/rest-apis/v2/approval-engine/definitions/get-definition.mdx b/api-reference/rest-apis/v2/approval-engine/definitions/get-definition.mdx index cd4997c2c..19013c123 100644 --- a/api-reference/rest-apis/v2/approval-engine/definitions/get-definition.mdx +++ b/api-reference/rest-apis/v2/approval-engine/definitions/get-definition.mdx @@ -63,9 +63,26 @@ Use this API to fetch a workflow definition. Returns the full `DefinitionView`. "description": null, "version": 1, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [ /* full node array */ ], - "edges": [ /* full edge array */ ], - "groups": [ /* full group array */ ], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } }, + { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u_reviewer_01", "mandatory": true }] } }, + { "nodeId": "agent-publish", "type": "agent", "config": { "agentId": "publish-agent-v1" } }, + { "nodeId": "agent-rework", "type": "agent", "config": { "agentId": "rework-agent-v1" } } + ], + "edges": [ + { "from": "agent-draft", "to": "human-review" }, + { "from": "human-review", "to": "agent-publish", "on": "approve" }, + { "from": "human-review", "to": "agent-rework", "on": "reject" } + ], + "groups": null, + "compiled": { + "forwardEdges": [ + { "from": "agent-draft", "to": "human-review", "role": "always", "when": null }, + { "from": "human-review", "to": "agent-publish", "role": "approve", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "approve"] } }, + { "from": "human-review", "to": "agent-rework", "role": "reject", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "reject"] } } + ], + "loops": [] + }, "triggers": null, "tags": null, "custom": null, @@ -76,6 +93,39 @@ Use this API to fetch a workflow definition. Returns the full `DefinitionView`. } ``` +#### The `compiled` block + +`compiled` is a read-only block on every `DefinitionView`. The authored `edges` echo `sourceEdges` byte-for-byte; `compiled` is the server-derived runtime view used to drive execution. Both are additive — render a workflow graph directly from `compiled.forwardEdges` instead of re-implementing group expansion client-side. + + + + + The runtime forward-edge list. Group endpoints are expanded to per-member compiled edges and `on` roles are compiled to a predicate AST. Each entry: + + | Field | Type | Notes | + | ------------- | ---------------------------------------------------------- | ------------------------------------------------------------------ | + | `from` | string | Resolved node id. | + | `to` | string | Resolved node id. | + | `role` | `approve` / `reject` / `always` / `exhausted` / `custom` | The authored `on` role. | + | `when` | `JsonAst \| null` | Deterministic predicate AST; `null` for `always`. | + | `fromGroupId` | string | Present when the authored `from` was a group. | + | `toGroupId` | string | Present when the authored `to` was a group. | + + + + The server-derived loop regions. Each entry: + + | Field | Type | Notes | + | ------------- | ------------------------------------------ | ------------------------------------------------------ | + | `loopId` | string | Stable identifier for the derived loop. | + | `entryNodeId` | string | Node re-entered on each iteration. | + | `bodyNodeIds` | string[] | Nodes inside the loop's iteration scope. | + | `maxIterations` | number | Hard cap per execution (from the reject edge's `loop`). | + | `onExhausted` | `{ routeToNodeId: string } \| null` | Sibling `on:"exhausted"` target, or `null`. | + + + + #### Failure Response ```JSON @@ -98,9 +148,12 @@ Use this API to fetch a workflow definition. Returns the full `DefinitionView`. "description": null, "version": 1, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } } + ], "edges": [], - "groups": [], + "groups": null, + "compiled": { "forwardEdges": [], "loops": [] }, "triggers": null, "tags": null, "custom": null, diff --git a/api-reference/rest-apis/v2/approval-engine/definitions/list-definitions.mdx b/api-reference/rest-apis/v2/approval-engine/definitions/list-definitions.mdx index 3df04155e..f6f3dc63f 100644 --- a/api-reference/rest-apis/v2/approval-engine/definitions/list-definitions.mdx +++ b/api-reference/rest-apis/v2/approval-engine/definitions/list-definitions.mdx @@ -77,12 +77,34 @@ Requires a Firestore composite index (`status` ASC, `updatedAt` DESC). The first ```JSON { "result": { - "items": [ /* DefinitionView[] */ ], + "items": [ + { + "definitionId": "marketing-copy-approval", + "name": "Marketing copy approval", + "description": null, + "version": 1, + "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } } + ], + "edges": [], + "groups": null, + "compiled": { "forwardEdges": [], "loops": [] }, + "triggers": null, + "tags": null, + "custom": null, + "createdAt": 1731432000000, + "updatedAt": 1731432000000, + "status": "active" + } + ], "nextCursor": 1714300000000 } } ``` +Each `DefinitionView` row includes the read-only `compiled` block (`compiled.forwardEdges` + `compiled.loops`) alongside the authored `edges`. See [Get Definition](/api-reference/rest-apis/v2/approval-engine/definitions/get-definition#the-compiled-block) for the `compiled` schema. + #### Failure Response ```JSON diff --git a/api-reference/rest-apis/v2/approval-engine/definitions/update-definition.mdx b/api-reference/rest-apis/v2/approval-engine/definitions/update-definition.mdx index 615451010..3cfe176d3 100644 --- a/api-reference/rest-apis/v2/approval-engine/definitions/update-definition.mdx +++ b/api-reference/rest-apis/v2/approval-engine/definitions/update-definition.mdx @@ -44,19 +44,15 @@ Accepts every field from [Create Definition](/api-reference/rest-apis/v2/approva - 1–100 nodes. + 1–100 nodes. Each node accepts optional cosmetic `name` (1–200 chars) and `description` (≤ 2000 chars) labels. - 0–500 edges. + 0–500 edges, unified `EdgeSchema`: `{ from, to, on?, when?, loop? }`. `from` / `to` are `EdgeEndpoint`s (bare node-id string, `{ kind: "node", nodeId }`, or `{ kind: "group", groupId }`); `on` ∈ `approve` / `reject` / `always` (default) / `exhausted` / `custom`; `when` is valid only on `on:"custom"`; `loop { maxIterations: 1–20 }` only on an `on:"reject"` back-edge. A `human` node must have an outgoing `on:"reject"` edge. See [the edge model](/ai/approval-engine/customize-behavior#edge-model) for full semantics and the [Create Definition](/api-reference/rest-apis/v2/approval-engine/definitions/create-definition) field table. - 0–100 parallel-group definitions. - - - - 0–20 loop region declarations. See [loop regions](/ai/approval-engine/customize-behavior#loop-regions) on the Customize Behavior page for the full schema and linter rules. + 0–100 parallel-group definitions. A `waitAll` or `cancelOnQuorum` group can be an edge source (`from: { kind: "group", groupId }`); group-to-node fan-out (`to: { kind: "group", groupId }`) is accepted for all policies. A forward `on:"reject"` from a `joinOnQuorum` / `cancelOnQuorum` group is rejected as a dead edge. See [parallel groups and quorum policies](/ai/approval-engine/customize-behavior#parallel-groups-and-quorum-policies). @@ -119,9 +115,26 @@ Every successful update increments `version` and snapshots the prior content. In "description": null, "version": 2, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [ /* current */ ], - "edges": [ /* current */ ], - "groups": [ /* current */ ], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } }, + { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u_reviewer_01", "mandatory": true }] } }, + { "nodeId": "agent-publish", "type": "agent", "config": { "agentId": "publish-agent-v1" } }, + { "nodeId": "agent-rework", "type": "agent", "config": { "agentId": "rework-agent-v1" } } + ], + "edges": [ + { "from": "agent-draft", "to": "human-review" }, + { "from": "human-review", "to": "agent-publish", "on": "approve" }, + { "from": "human-review", "to": "agent-rework", "on": "reject" } + ], + "groups": null, + "compiled": { + "forwardEdges": [ + { "from": "agent-draft", "to": "human-review", "role": "always", "when": null }, + { "from": "human-review", "to": "agent-publish", "role": "approve", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "approve"] } }, + { "from": "human-review", "to": "agent-rework", "role": "reject", "when": { "op": "eq", "args": [{ "var": "output.decision" }, "reject"] } } + ], + "loops": [] + }, "triggers": null, "tags": null, "custom": null, @@ -143,7 +156,7 @@ Every successful update increments `version` and snapshots the prior content. In } ``` -**Errors:** `NOT_FOUND` (definition does not exist) / `FAILED_PRECONDITION` (`ifVersion` mismatch) / `INVALID_ARGUMENT` (schema or linter failure). +**Errors:** `NOT_FOUND` (definition does not exist) / `FAILED_PRECONDITION` (`ifVersion` mismatch) / `INVALID_ARGUMENT` (schema, `compileGraph`, or linter failure). ```js @@ -154,9 +167,12 @@ Every successful update increments `version` and snapshots the prior content. In "description": null, "version": 2, "scope": { "level": "apiKey", "organizationId": null, "documentId": null }, - "nodes": [], + "nodes": [ + { "nodeId": "agent-draft", "type": "agent", "config": { "agentId": "copy-agent-v1" } } + ], "edges": [], - "groups": [], + "groups": null, + "compiled": { "forwardEdges": [], "loops": [] }, "triggers": null, "tags": null, "custom": null, diff --git a/api-reference/rest-apis/v2/approval-engine/steps/resolve-step.mdx b/api-reference/rest-apis/v2/approval-engine/steps/resolve-step.mdx index 5d0696b9a..884ffc985 100644 --- a/api-reference/rest-apis/v2/approval-engine/steps/resolve-step.mdx +++ b/api-reference/rest-apis/v2/approval-engine/steps/resolve-step.mdx @@ -72,7 +72,7 @@ Every resolve writes a `step.overridden` audit event with `{ actorId, action, re - **`reviewer-reject` and `force-reject` do not populate `output.rejectedBy` / `output.rejectorMandatory`.** Those fields are only set on the natural [Record Reviewer Decision](/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision) aggregator path. If you drive [loop regions](/ai/approval-engine/customize-behavior#loop-regions) via `reviewer-reject`, the default loop predicate (`decision == 'reject' && rejectorMandatory == true`) won't fire — supply an explicit `onIterationReject.when` predicate that filters on `decision` alone. + **`reviewer-reject` and `force-reject` do not populate `output.rejectedBy` / `output.rejectorMandatory`.** Those fields are only set on the natural [Record Reviewer Decision](/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision) aggregator path. The default loop predicate (`decision == 'reject' && rejectorMandatory == true`) on a [`reject`+`loop` back-edge](/ai/approval-engine/customize-behavior#edge-model) won't fire when you drive rejections through this override endpoint. Use the natural reviewer-decision path for loop-back workflows that depend on the default mandatory-reject predicate. ## **Example Requests** diff --git a/api-reference/sdk/models/data-models.mdx b/api-reference/sdk/models/data-models.mdx index ec32bc193..b6659709c 100644 --- a/api-reference/sdk/models/data-models.mdx +++ b/api-reference/sdk/models/data-models.mdx @@ -11039,9 +11039,9 @@ Request payload for `sdk.api.agents.createAgent`. Accepts additional passthrough | `name` | `string` | Yes | Agent display name. | | `description` | `string` | No | Human-readable agent description. | | `rawInstructions` | `string` | No | Unprocessed natural-language instructions. | -| `instructions` | `string` | No | Resolved/structured instructions. | +| `instructions` | `string` | No | Resolved/structured instructions. Required when the configured strategy is `ai`, `service+ai`, or `stagehand-agent`. | | `enabled` | `boolean` | No | Whether the agent is active. | -| `metadata` | `Record` | No | Arbitrary metadata. | +| `metadata` | `Record` | No | Arbitrary metadata. Server-managed metadata keys (`type`, `category`, `internal`, `apiKey`) are rejected on create/update-version payloads. | | `contextGathering` | `ContextGatheringConfig` | No | Context gathering configuration. | | `execution` | `AgentExecutionConfig` | No | Execution configuration. | | `response` | `AgentResponseConfig` | No | Response configuration. | @@ -11050,6 +11050,8 @@ Request payload for `sdk.api.agents.createAgent`. Accepts additional passthrough | `scope` | `AgentScopeConfig` | No | Scope configuration. | | `setup` | `AgentSetupConfig` | No | Setup configuration. | +Create and update-version payloads also reject the top-level server-managed `managedBy` field with `400 INVALID_ARGUMENT`. + ### `UpdateAgentRequest` (Node) Request payload for `sdk.api.agents.updateAgent`. @@ -11063,13 +11065,13 @@ Request payload for `sdk.api.agents.updateAgent`. ### `UpdateAgentVersionRequest` (Node) -Request payload for `sdk.api.agents.updateAgentVersion`. Accepts additional passthrough fields. +Request payload for `sdk.api.agents.updateAgentVersion`. Accepts additional passthrough fields. Partial updates deep-merge nested config objects, so omitted siblings under `execution`, `contextGathering`, and similar blocks are preserved. | Field | Type | Required | Description | |-------|------|----------|-------------| | `agentId` | `string` | Yes | Agent to version. | | `rawInstructions` | `string` | No | Unprocessed natural-language instructions. | -| `instructions` | `string` | No | Resolved/structured instructions. | +| `instructions` | `string` | No | Resolved/structured instructions. Required when the configured strategy is `ai`, `service+ai`, or `stagehand-agent`. | | `phaseTimeoutMs` | `number` | No | Positive integer phase timeout in ms. | | `contextGathering` | `ContextGatheringConfig` | No | Context gathering configuration. | | `execution` | `AgentExecutionConfig` | No | Execution configuration. | @@ -11079,6 +11081,8 @@ Request payload for `sdk.api.agents.updateAgentVersion`. Accepts additional pass | `scope` | `AgentScopeConfig` | No | Scope configuration. | | `setup` | `AgentSetupConfig` | No | Setup configuration. | +Update-version payloads also reject the top-level server-managed `managedBy` field and server-managed metadata keys (`metadata.type`, `metadata.category`, `metadata.internal`, `metadata.apiKey`) with `400 INVALID_ARGUMENT`. + ### `DeleteAgentRequest` (Node) Request payload for `sdk.api.agents.deleteAgent`. @@ -11281,6 +11285,76 @@ Request payload for `sdk.api.agents.removeAgentsFromGroup`. | `groupId` | `string` | Yes | Target group. | | `agentIds` | `string[]` | Yes | Agents to remove, 1..100. | +### Agent configuration nested types (Node) + +These config types are referenced by `CreateAgentRequest` and `UpdateAgentVersionRequest`. + +```ts +interface ResponseFieldDescriptions { + summary?: string; + title?: string; + description?: string; + severity?: string; + targetText?: string; + suggestion?: string; + isPageLevel?: string; + issueType?: string; + confidence?: string; + htmlSelector?: string; + findingType?: string; +} + +type RestApiAuthConfig = + | { type: 'none' } + | { type: 'bearer'; token: string } + | { type: 'basic'; username: string; password: string } + | { type: 'header'; headers: Record }; + +interface RestApiEndpoint { + url: string; + description: string; + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + auth?: RestApiAuthConfig; + headers?: Record; + query?: Record; + body?: unknown; + cacheTtlSeconds?: number; // default 0, max 86400 + timeoutMs?: number; // default 10000, max 30000 + responsePath?: string; +} + +interface RestApiContextStrategyOptions { + endpoints: RestApiEndpoint[]; // 1..10 + maxResponseBytes?: number; // default 1000000, max 5000000 +} + +interface ContextGatheringConfig { + strategies?: string[]; // include 'rest-api' to inject {{restApiData}} + strategyOptions?: { + 'rest-api'?: RestApiContextStrategyOptions; + } & Record>; +} + +type McpAuthConfig = { type: 'none' } | { type: 'header'; headers: Record }; + +interface McpServerConfig { + url: string; + auth?: McpAuthConfig; + allowedTools?: string[]; +} + +interface AgentExecutionConfig { + executionStrategy?: 'default' | 'ai' | 'service+ai' | 'stagehand-agent' | 'mcp-tools' | string; + mcpServers?: McpServerConfig[]; + responseDescriptions?: ResponseFieldDescriptions; + serviceId?: string; + strategyOptions?: Record>; + knowledge?: Record; +} +``` + +Secret-bearing `rest-api` and `mcp-tools` auth fields are encrypted at rest and returned redacted on read paths, including agent version snapshots. + ### `MemorySearchRequest` (Node) Request payload for `sdk.api.memory.search`. @@ -11534,9 +11608,8 @@ Request payload for `sdk.api.approval.createDefinition`. | `description` | `string` | No | Description, up to 2000 chars. | | `scope` | `ApprovalScope` | No | Definition scope. | | `nodes` | `ApprovalNode[]` | Yes | Workflow nodes, 1..100. | -| `edges` | `ApprovalEdge[]` | Yes | Workflow edges; may be empty; max 500. | +| `edges` | `ApprovalEdge[]` | Yes | Workflow edges (unified `EdgeSchema`: `from`/`to`/`on`/`when`/`loop`); may be empty; max 500. Reject paths and loop-backs are `on:'reject'` edges. | | `groups` | `ApprovalGroup[]` | No | Node groups. | -| `loops` | `ApprovalLoop[]` | No | Loop definitions. | | `triggers` | `ApprovalTrigger[]` | No | Trigger definitions. | | `tags` | `string[]` | No | Tags. | | `custom` | `Record` | No | Arbitrary custom data. | @@ -11583,6 +11656,70 @@ Request payload for `sdk.api.approval.listDefinitions`. | `organizationId` | `string` | No | Organization context. | | `documentId` | `string` | No | Document context. | +### Approval graph nested types (Node) + +These graph types are referenced by approval definition requests and `DefinitionView` responses. + +```ts +type JsonAst = Record; + +type ApprovalEdgeEndpoint = string | { kind: 'node'; nodeId: string } | { kind: 'group'; groupId: string }; + +interface ApprovalEdge { + from: ApprovalEdgeEndpoint; + to: ApprovalEdgeEndpoint; + on?: 'approve' | 'reject' | 'always' | 'exhausted' | 'custom'; // default 'always' + when?: string; // JSON-AST string, only valid when on === 'custom' + loop?: { maxIterations: number }; // 1..20, only on an on:'reject' back-edge +} + +interface ApprovalNodeBase { + nodeId: string; + type: 'agent' | 'human' | 'webhook'; + slaMs?: number; + requireNonEmptyOutput?: boolean; + name?: string; // 1..200 + description?: string; // max 2000 +} + +type ApprovalNode = + | (ApprovalNodeBase & { type: 'agent'; config: Record }) + | (ApprovalNodeBase & { + type: 'human'; + config: { + reviewerIds?: string[]; + reviewers?: Array<{ userId: string; mandatory: boolean }>; + reviewerEmails?: string[]; + commentBody?: string; + }; + }) + | (ApprovalNodeBase & { type: 'webhook'; config: Record }); + +interface ApprovalCompiledForwardEdge { + from: string; + to: string; + role: 'approve' | 'reject' | 'always' | 'exhausted' | 'custom'; + when: JsonAst | null; + fromGroupId?: string; + toGroupId?: string; +} + +interface ApprovalCompiledLoop { + loopId: string; + entryNodeId: string; + bodyNodeIds: string[]; + maxIterations: number; + onExhausted: { routeToNodeId: string } | null; +} + +interface ApprovalCompiledGraph { + forwardEdges: ApprovalCompiledForwardEdge[]; + loops: ApprovalCompiledLoop[]; +} +``` + +Every `DefinitionView` returned by create, get, and list includes read-only `compiled.forwardEdges` and `compiled.loops` alongside the authored `edges`. + ### `ApprovalDispatchExecutionRequest` (Node) Request payload for `sdk.api.approval.dispatchExecution`. diff --git a/backend-sdks/node.mdx b/backend-sdks/node.mdx index ebff725c9..97952f3ab 100644 --- a/backend-sdks/node.mdx +++ b/backend-sdks/node.mdx @@ -3226,6 +3226,10 @@ await sdk.api.token.getToken('org-123', 'user-1', 'john@example.com', false); } ``` + +Custom agent config supports the `rest-api` context-gathering strategy and the `mcp-tools` execution strategy. Secret-bearing REST API and MCP auth fields are encrypted at rest and redacted on read paths. Create and update-version payloads reject server-managed fields such as `managedBy`, `metadata.type`, `metadata.category`, `metadata.internal`, and `metadata.apiKey`; strategies that require instructions (`ai`, `service+ai`, `stagehand-agent`) reject empty `instructions`. + + {/* ### Agents @@ -3493,8 +3497,13 @@ These shared/nested config interfaces are referenced by the request types above. ```ts interface ResponseFieldDescriptions { summary?: string; title?: string; description?: string; severity?: string; targetText?: string; suggestion?: string; isPageLevel?: string; issueType?: string; confidence?: string; htmlSelector?: string; findingType?: string; } interface KnowledgeConfig { useMemory?: boolean; maxChunks?: number; /* 1..20 * / maxPatterns?: number; /* 1..20 * / maxActivities?: number; /* 1..20 * / } -interface ContextGatheringConfig { strategies?: string[]; /* non-empty when present * / strategyOptions?: Record>; } -interface AgentExecutionConfig { responseDescriptions?: ResponseFieldDescriptions; executionStrategy?: string; serviceId?: string; strategyOptions?: Record>; knowledge?: KnowledgeConfig; } +type RestApiAuthConfig = { type: 'none' } | { type: 'bearer'; token: string } | { type: 'basic'; username: string; password: string } | { type: 'header'; headers: Record }; +interface RestApiEndpoint { url: string; description: string; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; auth?: RestApiAuthConfig; headers?: Record; query?: Record; body?: unknown; cacheTtlSeconds?: number; /* max 86400 */ timeoutMs?: number; /* max 30000 */ responsePath?: string; } +interface RestApiContextStrategyOptions { endpoints: RestApiEndpoint[]; /* 1..10 */ maxResponseBytes?: number; /* default 1000000, max 5000000 */ } +interface ContextGatheringConfig { strategies?: string[]; /* includes 'rest-api'; non-empty when present * / strategyOptions?: { 'rest-api'?: RestApiContextStrategyOptions } & Record>; } +type McpAuthConfig = { type: 'none' } | { type: 'header'; headers: Record }; +interface McpServerConfig { url: string; auth?: McpAuthConfig; allowedTools?: string[]; } +interface AgentExecutionConfig { responseDescriptions?: ResponseFieldDescriptions; executionStrategy?: 'default' | 'ai' | 'service+ai' | 'stagehand-agent' | 'mcp-tools' | string; serviceId?: string; strategyOptions?: Record>; knowledge?: KnowledgeConfig; mcpServers?: McpServerConfig[]; } interface AgentResponseConfig { useAiFormatting?: boolean; formattingPrompt?: string; responseAdapter?: string; } interface AgentPostProcessConfig { guardrails?: { enabled?: boolean }; matchAndMerge?: { enabled?: boolean }; pinResolution?: { enabled?: boolean }; annotations?: { enabled?: boolean; strategy?: string }; analytics?: { enabled?: boolean }; customProcessors?: string[]; } interface AgentInputUserContextField { id: string; title: string; type: 'string' | 'number' | 'boolean'; required?: boolean; defaultValue?: string | number | boolean; } @@ -3783,7 +3792,7 @@ A new service for defining approval/review workflows (graphs of agent, human, an #### `createDefinition` -- Create a workflow definition (nodes, edges, groups, loops, triggers). +- Create a workflow definition (nodes, edges, groups, triggers). - Params: [ApprovalCreateDefinitionRequest](/api-reference/sdk/models/data-models#approvalcreatedefinitionrequest-node) - Returns: [VeltApiResponse](/api-reference/sdk/models/data-models#veltapiresponse-node) @@ -3794,8 +3803,9 @@ await sdk.api.approval.createDefinition({ scope: { level: 'organization', organizationId: 'org-123' }, nodes: [ { nodeId: 'review', type: 'human', config: { reviewers: [{ userId: 'user-1', mandatory: true }], commentBody: 'Please review before publishing.' } }, + { nodeId: 'rework', type: 'agent', config: { agentId: '__mock__', urlPath: 'documentUrl' } }, ], - edges: [], + edges: [{ from: 'review', to: 'rework', on: 'reject' }], }); ``` @@ -3811,8 +3821,9 @@ await sdk.api.approval.updateDefinition({ name: 'Document Publish Review v2', nodes: [ { nodeId: 'review', type: 'human', config: { reviewers: [{ userId: 'user-1', mandatory: true }], commentBody: 'Please review before publishing.' } }, + { nodeId: 'rework', type: 'agent', config: { agentId: '__mock__', urlPath: 'documentUrl' } }, ], - edges: [], + edges: [{ from: 'review', to: 'rework', on: 'reject' }], ifVersion: 1, }); ``` @@ -3942,26 +3953,26 @@ await sdk.api.approval.recordReviewerDecision({ executionId: 'exec-1', stepId: ' These node/graph/config interfaces are referenced by the request types above. They are not standalone data-model entries. ```ts +type JsonAst = Record; interface ApprovalScope { level: 'apiKey' | 'organization' | 'document'; organizationId?: string; documentId?: string; } interface ApprovalAgentResolutionPolicy { kind: 'allResolved' | 'minResolved'; minCount?: number; } // minCount req. when 'minResolved' (1..500) interface ApprovalAgentNodeConfig { agentId: string; urlPath: string; /* 1..500 */ crossPageExecute?: boolean; maxUrlsToProcess?: number; /* positive int, max 500 */ userContextMapping?: Record; pollIntervalMs?: number; /* 5000..60000 */ promptOverride?: string; /* max 8000 */ inputMapping?: Record; requireNonEmptyOutput?: boolean; agentMaxRuntimeMs?: number; /* positive int, max 24h */ blocking?: boolean; resolutionPolicy?: ApprovalAgentResolutionPolicy; /* required when blocking */ } interface ApprovalHumanReviewer { userId: string; mandatory: boolean; } -interface ApprovalHumanNodeOnRejectLoopBack { toNodeId: string; /* 1..64 */ maxIterations?: number; /* 1..20 */ onExhausted?: { routeToNodeId?: string }; when?: string; /* max 2000 */ } -interface ApprovalHumanNodeOnReject { routeToNodeId?: string; loopBack?: ApprovalHumanNodeOnRejectLoopBack; } // exactly one -interface ApprovalHumanNodeConfig { reviewerIds?: string[]; /* mutually exclusive with reviewers; 1..50 */ reviewers?: ApprovalHumanReviewer[]; /* unique userIds; >=1 mandatory; 1..50 */ reviewerEmails?: string[]; /* max 50 */ commentBody?: string; /* max 8000 */ onReject?: ApprovalHumanNodeOnReject; } +interface ApprovalHumanNodeConfig { reviewerIds?: string[]; /* mutually exclusive with reviewers; 1..50 */ reviewers?: ApprovalHumanReviewer[]; /* unique userIds; >=1 mandatory; 1..50 */ reviewerEmails?: string[]; /* max 50 */ commentBody?: string; /* max 8000 */ } // reject path is an outgoing on:'reject' edge interface ApprovalWebhookNodeConfig { url: string; /* https, allowed host, max 2000 */ method?: 'GET' | 'POST'; mode?: 'sync' | 'async'; authMode?: 'hmac' | 'token' | 'none'; authTokenHeader?: string; /* required when authMode === 'token'; max 500 */ timeoutMs?: number; /* 1000..60000 */ expectedStatusCodes?: number[]; /* each 100..599; max 20 */ bodyTemplate?: 'envelope' | 'pass-through' | 'none'; /* 'none' requires method GET */ requestHeaders?: Record; /* reserved/x-velt-* headers rejected; values max 2000 */ } -interface ApprovalAgentNode { nodeId: string; type: 'agent'; config: ApprovalAgentNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; } -interface ApprovalHumanNode { nodeId: string; type: 'human'; config: ApprovalHumanNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; } -interface ApprovalWebhookNode { nodeId: string; type: 'webhook'; config: ApprovalWebhookNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; } +interface ApprovalAgentNode { nodeId: string; type: 'agent'; config: ApprovalAgentNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; name?: string; /* 1..200 */ description?: string; /* max 2000 */ } +interface ApprovalHumanNode { nodeId: string; type: 'human'; config: ApprovalHumanNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; name?: string; /* 1..200 */ description?: string; /* max 2000 */ } +interface ApprovalWebhookNode { nodeId: string; type: 'webhook'; config: ApprovalWebhookNodeConfig; slaMs?: number; requireNonEmptyOutput?: boolean; name?: string; /* 1..200 */ description?: string; /* max 2000 */ } type ApprovalNode = ApprovalAgentNode | ApprovalHumanNode | ApprovalWebhookNode; -interface ApprovalEdge { from: string; to: string; when?: string; } // from/to 1..64; when max 1000 +type ApprovalEdgeEndpoint = string | { kind: 'node'; nodeId: string } | { kind: 'group'; groupId: string }; +interface ApprovalEdge { from: ApprovalEdgeEndpoint; to: ApprovalEdgeEndpoint; on?: 'approve' | 'reject' | 'always' | 'exhausted' | 'custom'; /* default 'always' */ when?: string; /* JSON-AST; only valid when on==='custom' */ loop?: { maxIterations: number /* 1..20 */ }; /* only on an on:'reject' back-edge */ } type ApprovalQuorumPolicy = 'waitAll' | 'cancelOnQuorum' | 'joinOnQuorum'; interface ApprovalGroup { groupId: string; /* 1..64 */ memberNodeIds: string[]; /* 1..500 */ expectedSteps: number; /* 1..500 */ quorum: number; /* 1..500 */ onQuorumMet?: ApprovalQuorumPolicy; requiredNodeIds?: string[]; /* max 500 */ } +interface ApprovalCompiledForwardEdge { from: string; to: string; role: 'approve' | 'reject' | 'always' | 'exhausted' | 'custom'; when: JsonAst | null; fromGroupId?: string; toGroupId?: string; } +interface ApprovalCompiledLoop { loopId: string; entryNodeId: string; bodyNodeIds: string[]; maxIterations: number; onExhausted: { routeToNodeId: string } | null; } +interface ApprovalCompiledGraph { forwardEdges: ApprovalCompiledForwardEdge[]; loops: ApprovalCompiledLoop[]; } // read-only on DefinitionView responses interface ApprovalInboundWebhookTrigger { authMode: 'hmac' | 'bearer'; secret: string; /* 16..512 */ idempotencyHeader?: string; /* 1..128 */ idempotencyBodyPath?: string; /* 1..256 */ payloadMapping?: 'pass-through' | 'wrap'; } interface ApprovalTrigger { triggerId: string; /* 1..128 */ eventName?: string; /* max 128 */ filters?: Record; inboundWebhook?: ApprovalInboundWebhookTrigger; } -interface ApprovalLoopRejectTrigger { when?: string; } // max 2000 -interface ApprovalLoopExhaustionPolicy { routeToNodeId?: string; } // 1..64 -interface ApprovalLoop { loopId: string; /* 1..64 */ entryNodeId: string; /* 1..64 */ bodyNodeIds: string[]; /* 1..50 */ onIterationReject?: ApprovalLoopRejectTrigger; onExhausted?: ApprovalLoopExhaustionPolicy; maxIterations: number; /* 1..20 */ } interface ApprovalWebhookConfig { url: string; secret: string; eventTypes?: string[]; } // url https/max2000; secret 16..512; eventTypes each 1..128, max 50 ``` diff --git a/docs.json b/docs.json index de327eee5..14bd21833 100644 --- a/docs.json +++ b/docs.json @@ -308,6 +308,7 @@ "async-collaboration/activity/customize-behavior" ] }, + "ai/custom-agents", "ai/agent-comments", "ai/chat-sdk-adapter" ] diff --git a/release-notes/version-6/sdk-changelog.mdx b/release-notes/version-6/sdk-changelog.mdx index 2d2126a83..d619c88fb 100644 --- a/release-notes/version-6/sdk-changelog.mdx +++ b/release-notes/version-6/sdk-changelog.mdx @@ -11,7 +11,11 @@ description: Release Notes of changes added to the core Velt SDK -First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backward compatible — existing integrations work unchanged. +First beta of the modular SDK. The public `Velt` / `Snippyly` browser API is fully backward compatible — existing browser integrations work unchanged. Approval Engine definition authors should review the breaking `edges[]` contract change below. + +### Breaking Changes + +- [**Approval Engine**]: Workflow transitions are now expressed through a single unified `edges[]` array. The per-human-node `onReject` shorthand and the authored top-level `loops[]` array have been removed — a human node's reject path is now an outgoing edge, and loop-backs are reject edges marked with `loop`. Definitions using `onReject` or `loops[]` are rejected at write time with `INVALID_ARGUMENT`; already-stored definitions and in-flight executions are unaffected. See the upgrade guide for the migration mapping. [Learn more →](/release-notes/version-6/upgrade-guide) ### New Features @@ -25,6 +29,18 @@ First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backw - [**Memory**]: `knowledge/search` now strictly validates `includeRules` — non-boolean values (e.g. the string `"true"` or `1`) are rejected with a `400` instead of being silently ignored. [Learn more →](/api-reference/rest-apis/v2/memory/knowledge/search) +- [**Approval Engine**]: Every `DefinitionView` (on create, get, and list) now carries a read-only `compiled` block — `compiled.forwardEdges` is the runtime forward-edge list the engine drives execution from (group endpoints expanded, edge roles compiled), and `compiled.loops` is the server-derived loop region list. Clients can render the runtime graph without re-implementing the compiler. The authored `edges` still echo back byte-for-byte. Additive. [Learn more →](/api-reference/rest-apis/v2/approval-engine/definitions/get-definition) + +- [**Approval Engine**]: `waitAll` and `cancelOnQuorum` groups can now be edge sources. A `waitAll` source produces a collective approve/reject fan-out with unanimity (approve only if every member approved, else reject); a `cancelOnQuorum` source fires one collective approve-successor on quorum. Edge-to-group fan-out is accepted for all quorum policies. [Learn more →](/ai/approval-engine/customize-behavior) + +- [**Approval Engine**]: Nodes accept optional cosmetic `name` (1–200 chars) and `description` (max 2000 chars) fields, echoed verbatim in `NodeView` for stable per-node labels in visual editors. Both are additive and have no runtime effect. [Learn more →](/api-reference/rest-apis/v2/approval-engine/definitions/create-definition) + +- [**Agents**]: New `rest-api` context-gathering strategy for custom agents. An agent can fetch up to 10 customer-defined REST endpoints at execution time and inject the results into the prompt via the `{{restApiData}}` template variable. Outbound requests are SSRF-guarded, and endpoint auth secrets are encrypted at rest and redacted on every read path. + +- [**Agents**]: New `mcp-tools` execution strategy for custom agents. The agent connects to one or more customer-configured MCP servers and runs a multi-turn tool-calling loop (with per-server tool allowlists and a max-turn cap) before returning a structured response. Provider-agnostic across Claude and Gemini. + +- [**Agents**]: New `docs-code-comparison` built-in agent that verifies code snippets on a landing page against the docs site using the MCP tool loop, reporting mismatches as annotations. + ### Improvements - [**Core**]: `getXElement()` accessors (e.g. `getCommentElement()`) now return non-blocking lazy facades that kick off the chunk load and return immediately, preserving the synchronous, never-null contract — void calls are queued and flushed, promise calls await the load, and `on(...)` / observable subscriptions bridge through once the chunk lands. Each accessor also auto-enables its feature. Existing code compiles and runs unchanged. [Learn more →](/api-reference/sdk/api/api-methods) @@ -37,10 +53,28 @@ First beta of the modular SDK. The public `Velt` / `Snippyly` API is fully backw - [**Core**]: New lazy element-facade classes (`LazyXElement`, e.g. `LazyCommentElement`) returned by `getXElement()` and cast to the public element types — no change to your code. [Learn more →](/api-reference/sdk/api/api-methods) +- [**Core**]: `ApiKeySecretCipher` now centralizes the shared `${apiKey}:${salt}` AES-GCM encrypt/decrypt pattern used for customer LLM provider keys and REST API auth secrets. `CryptoUtil` now lives under `core/crypto/`, and CRDT imports it directly from that layer. No runtime behavior change. + +- [**Approval Engine**]: Structural validations that depend on authored edge provenance now run in `compileGraph` before loop extraction, while DAG integrity and group/loop-shape checks run on the compiled forward-only graph. This keeps new `APPROVAL_EDGE_*` errors separate from linter graph-shape errors and removes order-dependent validation behavior. + +- [**Agents**]: Partial updates to a custom agent config now deep-merge instead of shallow-merge, so nested fields (e.g. `execution.mcpServers`) are preserved when you update siblings. Create and update-version payloads now also reject server-managed fields (`managedBy`, `metadata.*`) with a per-key `400`. + +- [**Agents**]: Customer LLM API-key lookup and decryption now live in shared `ProviderApiKeyService`, so Rewriter, agents, and future LLM consumers use the same provider fallback and decrypt-failure analytics behavior. + +- [**Agents**]: Claude and Gemini providers now implement `askAIWithTools(request, executor)` for capped, provider-agnostic tool loops. Token usage is accumulated across turns, tool errors are forwarded back to the model, and callers fail with `AI_TOOLS_NOT_SUPPORTED` when the selected provider does not implement tool calling. + +- [**Agents**]: MCP client connections now enforce per-server tool allowlists, send `mcp-client-name` / `mcp-client-version` metadata headers, and return structured tool errors to the model instead of throwing opaque tool-call failures. + ### Bug Fixes - [**Recorder**]: The video player now loads transcription/subtitle embeds when their chunk is deferred — it proactively ensures the recorder chunk is loaded, upgrading the `` / `` elements so subtitles and transcription work even when the recorder chunk wasn't eagerly loaded. Fire-and-forget, idempotent, errors logged. [Learn more →](/api-reference/sdk/api/api-methods) - [**Reactions**]: The reaction pin active-state highlight is now reliable under the Angular Elements lifecycle. `` tracks its per-`annotationId` subscription separately and tears down only that subscription on `annotationId` change (not the blanket unsubscribe), so the user subscription survives and the current user's reaction pin reliably shows its active highlight (`active` class / `aria-pressed`). [Learn more →](/api-reference/sdk/api/api-methods) +- [**Memory**]: The email digest project-card count is now deterministic within a UTC day. Two digest runs over identical data on the same UTC day now produce the same project list and card names — the ask runs at temperature 0, the recency window is anchored to UTC-day boundaries, and the grounding gate keys only on deterministic signals. + +- [**Agents**]: Agent create and update now correctly return `400` when `instructions` are missing for strategies that require them (`ai`, `service+ai`, `stagehand-agent`). + +- [**Agents**]: Agent version snapshots (`listVersions`) now redact encrypted auth secrets fail-closed — no `{ __enc }` ciphertext leaks on the read path, and the call rethrows rather than returning unredacted data if redaction fails. + diff --git a/release-notes/version-6/upgrade-guide.mdx b/release-notes/version-6/upgrade-guide.mdx index d5c9a1e6e..5c519a7f6 100644 --- a/release-notes/version-6/upgrade-guide.mdx +++ b/release-notes/version-6/upgrade-guide.mdx @@ -5,15 +5,60 @@ description: Upgrade to Velt SDK v6.0.0-beta.1 and opt into modular feature chun ## Overview - **Modular SDK**: The SDK now ships as an eager `velt.js` core (bootstrap + facade) plus 19 lazy-loaded feature chunks (comment, cursor, presence, huddle, recorder, notification, reaction, and more). Each feature is fetched on demand instead of bundled into one eager payload. -- **Backward compatible**: The public `Velt` / `Snippyly` API is unchanged. Existing integrations work as-is. +- **Modular SDK backward compatibility**: The public `Velt` / `Snippyly` browser API is unchanged. Existing browser integrations work as-is unless they also author Approval Engine definitions with the legacy rejection contract. - **New opt-in control APIs**: Use `featureAllowList` at init to preload only the features you use, and `preload()` to warm a chunk or load tag-only features ahead of time. ## Breaking Changes -**None — fully backward compatible.** Existing integrations work unchanged. If you do not pass `featureAllowList`, the SDK preserves pre-modular behavior and preloads all chunks in the background. +- [**Modular SDK**] None. The public `Velt` / `Snippyly` API is unchanged. If you do not pass `featureAllowList`, the SDK preserves pre-modular behavior and preloads all chunks in the background. + +- [**Approval Engine — unified `edges[]` contract**] The Approval Engine removed the per-human-node `onReject` shorthand and the authored top-level `loops[]` array. All workflow transitions — approve routing, reject routing, group fan-out/in, and loop-backs — are now expressed as a single unified `edges[]` array. Human nodes no longer carry any rejection config; a human node's reject path is an outgoing `on:"reject"` edge. + - **Who is affected:** any definition that POSTs `HumanNodeConfig.onReject` (`routeToNodeId` or `loopBack`) or a top-level `loops[]` array. These are now rejected at write time with `INVALID_ARGUMENT`. + - **Who is not affected:** already-stored definitions and in-flight executions. The runtime still consumes the compiled forward edges and derived loops unchanged. + - **Why:** the prior model had two ways to express rejection that desugared at write time into a form callers never saw, breaking round-trip symmetry. Under the unified model, `POST.edges === GET.edges`. ## How to Upgrade +### Approval Engine: migrate to `edges[]` + +Rewrite every `onReject` field and every `loops[]` entry as one or more `edges[]` entries before writing new definitions. + +| Old form | New edge equivalent | +| --- | --- | +| `"onReject": { "routeToNodeId": "rework-notice" }` | `{ "from": "human-review", "to": "rework-notice", "on": "reject" }` | +| `"onReject": { "loopBack": { "toNodeId": "agent-draft", "maxIterations": 3 } }` | `{ "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } }` | +| `"loops": [{ "loopId": "...", "entryNodeId": "agent-draft", "bodyNodeIds": [...], "maxIterations": 3 }]` | `{ "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } }` | +| `onReject.loopBack.onExhausted.routeToNodeId` | sibling `{ "from": "human-review", "to": "escalate", "on": "exhausted" }` edge | + +**New `EdgeSchema` fields:** + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `from` | `EdgeEndpoint` | yes | Bare node-id string, or `{ kind: "node", nodeId }`, or `{ kind: "group", groupId }`. | +| `to` | `EdgeEndpoint` | yes | Same shape as `from`. | +| `on` | `"approve" \| "reject" \| "always" \| "exhausted" \| "custom"` | no (default `"always"`) | Semantic role. `approve` / `reject` auto-compile their predicates. | +| `when` | JSON-AST string | only for `on:"custom"` | Custom predicate. Invalid on any other edge. | +| `loop` | `{ maxIterations: 1–20 }` | only on an `on:"reject"` back-edge | Marks the reject edge as a loop-back; the server derives the loop region. | + +**New `INVALID_ARGUMENT` compile error keys.** A definition that violates the unified contract is rejected with one of these keys in the error message: + +| Error key | Cause | +| --- | --- | +| `APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN` | `on:"custom"` without a non-empty `when`. | +| `APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM` | `when` supplied on a non-custom edge. | +| `APPROVAL_EDGE_LOOP_REQUIRES_REJECT` | `loop` on a non-reject edge. | +| `APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR` | `reject`+`loop` whose `to` is not an ancestor of `from`. | +| `APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP` | `on:"reject"` to an ancestor without `loop` (unmarked cycle). | +| `APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING` | `on:"exhausted"` with no sibling `reject`+`loop` from the same `from`. | +| `APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH` | Human node with no outgoing `on:"reject"` edge. | +| `APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN` | Both endpoints are group containers. | +| `APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP` | Forward `on:"reject"` from a `joinOnQuorum` / `cancelOnQuorum` group (dead edge — those policies fan out only on approval-quorum). | +| `APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM` | `reject`+`loop` back-edge from a non-`joinOnQuorum` group. | + +For the full edge model, group-as-edge-source semantics, and the read-only `compiled` block, see [Customize Behavior](/ai/approval-engine/customize-behavior) and [Create Definition](/api-reference/rest-apis/v2/approval-engine/definitions/create-definition). + +## How to Upgrade the Modular SDK + Adopting the modular SDK is optional. Update the package and your existing code keeps working. To opt into on-demand loading, use the following control APIs. ### 1. Preload only the features you use