@circuitwall/jarela 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
- package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +585 -58
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +125 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -2
- package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +27323 -27034
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js +8 -0
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
- package/.next/standalone/.next/server/chunks/1388.js +106 -36
- package/.next/standalone/.next/server/chunks/2068.js +47 -10
- package/.next/standalone/.next/server/chunks/210.js +6 -6
- package/.next/standalone/.next/server/chunks/2447.js +10 -5
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3042.js +10 -8
- package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
- package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
- package/.next/standalone/.next/server/chunks/3524.js +150 -67
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +91 -31
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/5908.js +63 -42
- package/.next/standalone/.next/server/chunks/6142.js +10 -2
- package/.next/standalone/.next/server/chunks/8461.js +10 -8
- package/.next/standalone/.next/server/chunks/9032.js +5 -5
- package/.next/standalone/.next/server/chunks/9164.js +487 -177
- package/.next/standalone/.next/server/chunks/9304.js +51 -12
- package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +84 -24
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +89 -21
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-2ef8d75d40eb4311.js} +10643 -10295
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2805 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +52 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +13 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/integrations/NetworkSection.tsx +2 -2
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/agents/citation-checker.test.ts +40 -2
- package/lib/agents/citation-checker.ts +56 -11
- package/lib/agents/context-boundary.ts +5 -6
- package/lib/agents/instruction-edits.test.ts +120 -0
- package/lib/agents/instruction-edits.ts +65 -42
- package/lib/agents/prepare/request.ts +0 -2
- package/lib/agents/prepare/system-prompt.test.ts +10 -0
- package/lib/agents/prepare/system-prompt.ts +8 -2
- package/lib/agents/proposals.test.ts +76 -0
- package/lib/agents/proposals.ts +13 -5
- package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
- package/lib/agents/run-thread.ts +24 -9
- package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
- package/lib/agents/warm-summary-background.ts +22 -2
- package/lib/agents/warm-topics.test.ts +155 -1
- package/lib/agents/warm-topics.ts +27 -7
- package/lib/bridges/dispatcher.ts +1 -0
- package/lib/bridges/message-role.test.ts +81 -0
- package/lib/bridges/message-role.ts +23 -10
- package/lib/bridges/types.ts +6 -0
- package/lib/bridges/whatsapp-link.test.ts +32 -0
- package/lib/bridges/whatsapp-link.ts +21 -0
- package/lib/bridges/whatsapp.inbound.test.ts +239 -0
- package/lib/bridges/whatsapp.sender.test.ts +80 -1
- package/lib/bridges/whatsapp.ts +76 -38
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/pricing/snapshot.test.ts +70 -0
- package/lib/pricing/snapshot.ts +10 -2
- package/lib/proxy/dispatcher.test.ts +119 -1
- package/lib/proxy/dispatcher.ts +22 -9
- package/lib/stores/agent-configs.ts +10 -5
- package/lib/stores/bridges.test.ts +81 -0
- package/lib/stores/bridges.ts +9 -8
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/stores/proxy-config.test.ts +120 -0
- package/lib/stores/proxy-config.ts +22 -4
- package/lib/stores/threads.ts +12 -8
- package/lib/tools/communications/calendar.test.ts +43 -2
- package/lib/tools/communications/calendar.ts +2 -0
- package/lib/tools/communications/gmail.test.ts +51 -1
- package/lib/tools/communications/gmail.ts +44 -2
- package/lib/tools/communications/outlook-calendar.ts +2 -0
- package/lib/tools/communications/outlook.test.ts +33 -2
- package/lib/tools/communications/outlook.ts +2 -0
- package/lib/tools/filesystem/edit-match.test.ts +133 -0
- package/lib/tools/filesystem/edit-match.ts +7 -2
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/tools/support/result-refs.test.ts +131 -0
- package/lib/tools/support/result-refs.ts +41 -9
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/lib/utils/source-link.test.ts +51 -0
- package/lib/utils/source-link.ts +45 -0
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/312542a5725ba0d5.css +0 -5
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
|
@@ -85,17 +85,28 @@ const SOURCE_PRODUCING_TOOLS_RESULT = new Set([
|
|
|
85
85
|
"web_search", "fetch_webpage", "web_fetch",
|
|
86
86
|
]);
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
// so
|
|
88
|
+
const TRACKING_PARAM_RE = /^(?:utm(?:_.*)?|fbclid|gclid|dclid|msclkid|mc_cid|mc_eid|igshid|ref_src)$/i;
|
|
89
|
+
|
|
90
|
+
// Drop tracking params and in-page anchors for URLs and normalize
|
|
91
|
+
// backslashes for paths so a citation that adds `?utm_source=...` or uses
|
|
92
|
+
// forward slashes on Windows still matches the visited entry. Identifying
|
|
93
|
+
// query params (Outlook `?ItemID=`, `watch?v=`) and hash routes (Gmail
|
|
94
|
+
// `#all/<id>`) are kept verbatim: they are case-sensitive and are the only
|
|
95
|
+
// thing that distinguishes two message permalinks. Also fold `~/...` to the
|
|
96
|
+
// resolved home dir so `~/Downloads/x` and `C:\Users\andre\Downloads\x` de-dupe.
|
|
92
97
|
export function normalizeSource(raw: string): string {
|
|
93
98
|
const trimmed = raw.trim();
|
|
94
99
|
if (!trimmed) return "";
|
|
95
100
|
if (/^https?:\/\//i.test(trimmed)) {
|
|
96
101
|
try {
|
|
97
102
|
const u = new URL(trimmed);
|
|
98
|
-
|
|
103
|
+
const base = `${u.protocol}//${u.host}${u.pathname}`.replace(/\/+$/, "").toLowerCase();
|
|
104
|
+
const params = [...u.searchParams]
|
|
105
|
+
.filter(([key]) => !TRACKING_PARAM_RE.test(key))
|
|
106
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
107
|
+
const query = params.length > 0 ? `?${new URLSearchParams(params).toString()}` : "";
|
|
108
|
+
const hashRoute = u.hash.includes("/") ? u.hash : "";
|
|
109
|
+
return base + query + hashRoute;
|
|
99
110
|
} catch {
|
|
100
111
|
return trimmed.toLowerCase();
|
|
101
112
|
}
|
|
@@ -436,12 +447,9 @@ ${sent}`;
|
|
|
436
447
|
export function parseCitationVerdict(text: string): CitationClaim[] | null {
|
|
437
448
|
if (!text) return null;
|
|
438
449
|
const stripped = text.replace(/```(?:json)?\s*/gi, "").replace(/```/g, "").trim();
|
|
439
|
-
const
|
|
440
|
-
if (!
|
|
441
|
-
|
|
442
|
-
try { parsed = JSON.parse(m[0]); } catch { return null; }
|
|
443
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
444
|
-
const claimsRaw = (parsed as { claims?: unknown }).claims;
|
|
450
|
+
const parsed = findClaimsObject(stripped);
|
|
451
|
+
if (!parsed) return null;
|
|
452
|
+
const claimsRaw = parsed.claims;
|
|
445
453
|
if (!Array.isArray(claimsRaw)) return null;
|
|
446
454
|
const claims: CitationClaim[] = [];
|
|
447
455
|
for (const item of claimsRaw) {
|
|
@@ -464,6 +472,43 @@ export function parseCitationVerdict(text: string): CitationClaim[] | null {
|
|
|
464
472
|
return claims;
|
|
465
473
|
}
|
|
466
474
|
|
|
475
|
+
// Models often wrap the verdict in prose that itself contains braces, so a
|
|
476
|
+
// single greedy `{...}` match can start or end in the wrong place. Try each
|
|
477
|
+
// `{` as a start and parse the balanced object it opens (string-aware).
|
|
478
|
+
function findClaimsObject(text: string): { claims?: unknown } | null {
|
|
479
|
+
for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
|
|
480
|
+
const end = balancedObjectEnd(text, start);
|
|
481
|
+
if (end === -1) continue;
|
|
482
|
+
const candidate = text.slice(start, end + 1);
|
|
483
|
+
if (!candidate.includes('"claims"')) continue;
|
|
484
|
+
try {
|
|
485
|
+
const parsed: unknown = JSON.parse(candidate);
|
|
486
|
+
if (parsed && typeof parsed === "object" && "claims" in parsed) return parsed as { claims?: unknown };
|
|
487
|
+
} catch { /* try the next opening brace */ }
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function balancedObjectEnd(text: string, start: number): number {
|
|
493
|
+
let depth = 0;
|
|
494
|
+
let inString = false;
|
|
495
|
+
for (let i = start; i < text.length; i++) {
|
|
496
|
+
const ch = text[i];
|
|
497
|
+
if (inString) {
|
|
498
|
+
if (ch === "\\") i++;
|
|
499
|
+
else if (ch === '"') inString = false;
|
|
500
|
+
} else if (ch === '"') {
|
|
501
|
+
inString = true;
|
|
502
|
+
} else if (ch === "{") {
|
|
503
|
+
depth++;
|
|
504
|
+
} else if (ch === "}") {
|
|
505
|
+
depth--;
|
|
506
|
+
if (depth === 0) return i;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return -1;
|
|
510
|
+
}
|
|
511
|
+
|
|
467
512
|
function collectStringsFromPayload(payload: unknown, out: Set<string>): void {
|
|
468
513
|
if (!payload) return;
|
|
469
514
|
if (typeof payload === "string") {
|
|
@@ -3,12 +3,11 @@ import { kickBoundaryCompaction } from "@/lib/agents/warm-summary-background";
|
|
|
3
3
|
|
|
4
4
|
// Every caller of moveThreadContextBoundary represents a user-initiated
|
|
5
5
|
// boundary change (drag the line, the manual /compact command, or the agent's
|
|
6
|
-
// compact_context tool acting on the user's say-so)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// choice doesn't get silently re-moved on the very next turn.
|
|
6
|
+
// compact_context tool acting on the user's say-so). Suppresses the
|
|
7
|
+
// hot-turn-limit compaction in lib/agents/run-thread.ts for the next N
|
|
8
|
+
// eligible turns so a manual choice doesn't get silently re-moved on the very
|
|
9
|
+
// next turn. The auto-detector arms a shorter cooldown of its own
|
|
10
|
+
// (hotTurnCooldownUntil) so it doesn't re-run on every turn either.
|
|
12
11
|
// Counted in messages (~2 per turn: one user + one assistant row) since
|
|
13
12
|
// that's what's already on the thread row — no extra query or per-turn
|
|
14
13
|
// write needed to check it.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyInstructionEdits } from "./instruction-edits";
|
|
3
|
+
|
|
4
|
+
function edit(base: string, edits: unknown): string {
|
|
5
|
+
const out = applyInstructionEdits(base, edits);
|
|
6
|
+
if (!out.ok) throw new Error(out.error);
|
|
7
|
+
return out.text;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("applyInstructionEdits validation", () => {
|
|
11
|
+
it("rejects a non-array and reports the failing index", () => {
|
|
12
|
+
expect(applyInstructionEdits("x", { op: "append" })).toEqual({ ok: false, error: "instructions_edits must be an array" });
|
|
13
|
+
expect(applyInstructionEdits("x", [{ op: "append", text: "a" }, null])).toEqual({ ok: false, error: "instructions_edits[1] must be an object" });
|
|
14
|
+
expect(applyInstructionEdits("x", [{}])).toEqual({ ok: false, error: "instructions_edits[0].op is required" });
|
|
15
|
+
expect(applyInstructionEdits("x", [{ op: "rewrite" }])).toEqual({ ok: false, error: "instructions_edits[0].op is not supported" });
|
|
16
|
+
expect(applyInstructionEdits("x", [{ op: "replace", find: "x" }])).toEqual({ ok: false, error: "instructions_edits[0].replace must be a string" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("fails the whole batch instead of applying a prefix of it", () => {
|
|
20
|
+
const out = applyInstructionEdits("keep", [{ op: "append", text: " more" }, { op: "remove" }]);
|
|
21
|
+
expect(out.ok).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("append / prepend", () => {
|
|
26
|
+
it("skips an if_missing append when the text is already present, honoring ignore_case", () => {
|
|
27
|
+
const out = applyInstructionEdits("Always cite sources.", [
|
|
28
|
+
{ op: "append", text: "always CITE sources.", if_missing: true, ignore_case: true },
|
|
29
|
+
{ op: "prepend", text: "Be brief. ", if_missing: true },
|
|
30
|
+
]);
|
|
31
|
+
expect(out).toEqual({
|
|
32
|
+
ok: true,
|
|
33
|
+
text: "Be brief. Always cite sources.",
|
|
34
|
+
summary: [
|
|
35
|
+
{ index: 0, op: "append", applied: false },
|
|
36
|
+
{ index: 1, op: "prepend", applied: true },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("replace / remove", () => {
|
|
43
|
+
it("treats find text literally, including regex metacharacters and $ patterns", () => {
|
|
44
|
+
expect(edit("cost is $5 (approx.)", [{ op: "replace", find: "$5 (approx.)", replace: "$$ & $&", ignore_case: true }]))
|
|
45
|
+
.toBe("cost is $$ & $&");
|
|
46
|
+
expect(edit("a.b a.b", [{ op: "replace", find: "a.b", replace: "$'", all: true }])).toBe("$' $'");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("replaces only the first match unless all is set, and counts replacements", () => {
|
|
50
|
+
const out = applyInstructionEdits("Foo foo FOO", [
|
|
51
|
+
{ op: "replace", find: "foo", replace: "bar", ignore_case: true },
|
|
52
|
+
{ op: "remove", text: "FOO", all: true },
|
|
53
|
+
]);
|
|
54
|
+
expect(out).toMatchObject({ ok: true, text: "bar foo ", summary: [{ replacements: 1 }, { removals: 1 }] });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is a no-op for an empty find string rather than inserting between every character", () => {
|
|
58
|
+
expect(edit("abc", [{ op: "replace", find: "", replace: "-", all: true }])).toBe("abc");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("dedupe_lines", () => {
|
|
63
|
+
it("removes repeated rules case-insensitively but keeps blank lines", () => {
|
|
64
|
+
const out = applyInstructionEdits("- Cite sources\n\n- cite sources \n- Be brief\n\n", [{ op: "dedupe_lines" }]);
|
|
65
|
+
expect(out).toMatchObject({ ok: true, text: "- Cite sources\n\n- Be brief\n\n", summary: [{ removed: 1 }] });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the last occurrence when asked", () => {
|
|
69
|
+
expect(edit("a\nb\nA", [{ op: "dedupe_lines", keep: "last" }])).toBe("b\nA");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("never removes code fence markers, fenced content, or horizontal rules", () => {
|
|
73
|
+
const base = [
|
|
74
|
+
"Rule one",
|
|
75
|
+
"```json",
|
|
76
|
+
"{ \"a\": 1 }",
|
|
77
|
+
"```",
|
|
78
|
+
"---",
|
|
79
|
+
"Example two:",
|
|
80
|
+
"```json",
|
|
81
|
+
"{ \"a\": 1 }",
|
|
82
|
+
"```",
|
|
83
|
+
"---",
|
|
84
|
+
"Rule one",
|
|
85
|
+
].join("\n");
|
|
86
|
+
const expected = base.split("\n").slice(0, -1).join("\n");
|
|
87
|
+
expect(edit(base, [{ op: "dedupe_lines" }])).toBe(expected);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("does not treat a fence line with an info string as the closing fence", () => {
|
|
91
|
+
const base = "```text\n```json\nx\nx\n```\n";
|
|
92
|
+
expect(edit(base, [{ op: "dedupe_lines" }])).toBe(base);
|
|
93
|
+
});
|
|
94
|
+
it("preserves CRLF line endings", () => {
|
|
95
|
+
expect(edit("a\r\nb\r\na\r\nc", [{ op: "dedupe_lines" }])).toBe("a\r\nb\r\nc");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("dedupe_paragraphs", () => {
|
|
100
|
+
it("removes a repeated paragraph and leaves the rest of the text byte-identical", () => {
|
|
101
|
+
const base = "# Rules\n\n - indented item\n - second\n\n\nAlways cite.\n\nalways cite.\n";
|
|
102
|
+
expect(edit(base, [{ op: "dedupe_paragraphs" }])).toBe("# Rules\n\n - indented item\n - second\n\n\nAlways cite.\n");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not touch whitespace when there is nothing to remove", () => {
|
|
106
|
+
const base = "\n\nIntro\n\n\n\n indented code\n";
|
|
107
|
+
const out = applyInstructionEdits(base, [{ op: "dedupe_paragraphs" }]);
|
|
108
|
+
expect(out).toMatchObject({ ok: true, text: base, summary: [{ removed: 0 }] });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("respects trim: false by treating differently indented paragraphs as distinct", () => {
|
|
112
|
+
const base = "item\n\n item";
|
|
113
|
+
expect(edit(base, [{ op: "dedupe_paragraphs", trim: false }])).toBe(base);
|
|
114
|
+
expect(edit(base, [{ op: "dedupe_paragraphs" }])).toBe("item");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("keeps the last occurrence and drops the separator that belonged to the removed paragraph", () => {
|
|
118
|
+
expect(edit("A\n\nB\n\na", [{ op: "dedupe_paragraphs", keep: "last" }])).toBe("B\n\na");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -172,58 +172,81 @@ function containsText(source: string, needle: string, ignoreCase: boolean): bool
|
|
|
172
172
|
return source.toLowerCase().includes(needle.toLowerCase());
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
176
|
+
const HORIZONTAL_RULE_RE = /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Drop duplicate segments from `parts`, which alternates content (even
|
|
180
|
+
* indexes) and the original separator that follows it (odd indexes).
|
|
181
|
+
* Separators are preserved verbatim, so only removed segments change the
|
|
182
|
+
* text. `keyOf` returns null for a segment that must always be kept.
|
|
183
|
+
*/
|
|
184
|
+
function dedupeSegments(
|
|
185
|
+
parts: readonly string[],
|
|
186
|
+
keyOf: (segmentIndex: number) => string | null,
|
|
187
|
+
keep: "first" | "last",
|
|
188
|
+
): { text: string; removed: number } {
|
|
189
|
+
const contentIdx = parts.map((_, i) => i).filter((i) => i % 2 === 0);
|
|
190
|
+
const visit = keep === "first" ? contentIdx : [...contentIdx].reverse();
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
const dropped = new Set<number>();
|
|
193
|
+
for (const idx of visit) {
|
|
194
|
+
const key = keyOf(idx);
|
|
195
|
+
if (key === null) continue;
|
|
196
|
+
if (seen.has(key)) dropped.add(idx);
|
|
197
|
+
else seen.add(key);
|
|
198
|
+
}
|
|
199
|
+
if (dropped.size === 0) return { text: parts.join(""), removed: 0 };
|
|
200
|
+
let text = "";
|
|
201
|
+
let emitted = false;
|
|
202
|
+
for (const idx of contentIdx) {
|
|
203
|
+
if (dropped.has(idx)) continue;
|
|
204
|
+
if (emitted && idx > 0) text += parts[idx - 1];
|
|
205
|
+
text += parts[idx];
|
|
206
|
+
emitted = true;
|
|
207
|
+
}
|
|
208
|
+
return { text, removed: dropped.size };
|
|
209
|
+
}
|
|
210
|
+
|
|
175
211
|
function dedupeLines(
|
|
176
212
|
source: string,
|
|
177
213
|
opts: { trim: boolean; ignoreCase: boolean; skipBlank: boolean; keep: "first" | "last" },
|
|
178
214
|
): { text: string; removed: number } {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
continue;
|
|
215
|
+
const parts = source.split(/(\r?\n)/);
|
|
216
|
+
// Fence markers, fenced code and horizontal rules repeat by design; a
|
|
217
|
+
// closing ``` is always a "duplicate" of the opening one.
|
|
218
|
+
const protectedIdx = new Set<number>();
|
|
219
|
+
let fence: string | null = null;
|
|
220
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
221
|
+
const marker = FENCE_RE.exec(parts[i])?.[1];
|
|
222
|
+
if (fence !== null) {
|
|
223
|
+
protectedIdx.add(i);
|
|
224
|
+
if (marker && marker[0] === fence[0] && marker.length >= fence.length && /^\s*[`~]+\s*$/.test(parts[i])) fence = null;
|
|
225
|
+
} else if (marker) {
|
|
226
|
+
protectedIdx.add(i);
|
|
227
|
+
fence = marker;
|
|
228
|
+
} else if (HORIZONTAL_RULE_RE.test(parts[i])) {
|
|
229
|
+
protectedIdx.add(i);
|
|
195
230
|
}
|
|
196
|
-
if (seen.has(key)) continue;
|
|
197
|
-
seen.add(key);
|
|
198
|
-
keep.add(idx);
|
|
199
231
|
}
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
return dedupeSegments(parts, (i) => {
|
|
233
|
+
if (protectedIdx.has(i)) return null;
|
|
234
|
+
const normalized = opts.trim ? parts[i].trim() : parts[i];
|
|
235
|
+
if (opts.skipBlank && normalized.length === 0) return null;
|
|
236
|
+
return opts.ignoreCase ? normalized.toLowerCase() : normalized;
|
|
237
|
+
}, opts.keep);
|
|
202
238
|
}
|
|
203
239
|
|
|
204
240
|
function dedupeParagraphs(
|
|
205
241
|
source: string,
|
|
206
242
|
opts: { trim: boolean; ignoreCase: boolean; keep: "first" | "last" },
|
|
207
243
|
): { text: string; removed: number } {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.
|
|
212
|
-
|
|
213
|
-
const base = opts.trim ? p.trim() : p;
|
|
244
|
+
const tail = /\s*$/.exec(source)?.[0] ?? "";
|
|
245
|
+
const parts = source.slice(0, source.length - tail.length).split(/(\r?\n(?:[ \t]*\r?\n)+)/);
|
|
246
|
+
const out = dedupeSegments(parts, (i) => {
|
|
247
|
+
if (parts[i].trim().length === 0) return null;
|
|
248
|
+
const base = opts.trim ? parts[i].trim() : parts[i];
|
|
214
249
|
return opts.ignoreCase ? base.toLowerCase() : base;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
? [...keys.keys()]
|
|
219
|
-
: [...keys.keys()].reverse();
|
|
220
|
-
const seen = new Set<string>();
|
|
221
|
-
for (const idx of visit) {
|
|
222
|
-
const key = keys[idx];
|
|
223
|
-
if (seen.has(key)) continue;
|
|
224
|
-
seen.add(key);
|
|
225
|
-
keep.add(idx);
|
|
226
|
-
}
|
|
227
|
-
const next = paragraphs.filter((_, idx) => keep.has(idx));
|
|
228
|
-
return { text: next.join("\n\n"), removed: paragraphs.length - next.length };
|
|
229
|
-
}
|
|
250
|
+
}, opts.keep);
|
|
251
|
+
return { text: out.text + tail, removed: out.removed };
|
|
252
|
+
}
|
|
@@ -38,8 +38,6 @@ export interface ThreadRunRequest {
|
|
|
38
38
|
_onCompacting?: () => void;
|
|
39
39
|
/** Internal — the submission path already committed this user's transcript row. */
|
|
40
40
|
_user_message_persisted?: boolean;
|
|
41
|
-
/** Thread message count captured before an eagerly persisted user turn. */
|
|
42
|
-
_message_count_before_user?: number;
|
|
43
41
|
/** Conversation idle duration captured before an eagerly persisted user turn. */
|
|
44
42
|
_idle_ms_before_user?: number | null;
|
|
45
43
|
/**
|
|
@@ -78,6 +78,8 @@ describe("buildSystemPrompt delivery channel", () => {
|
|
|
78
78
|
// The directive must reassure the agent it has access to the channel —
|
|
79
79
|
// that's the whole point of this block.
|
|
80
80
|
expect(prompt).toMatch(/DO have access to WhatsApp/);
|
|
81
|
+
// WhatsApp shows markdown links as raw text.
|
|
82
|
+
expect(prompt).toContain("WhatsApp does not render markdown links: write any link as the bare URL, even where a cite_as value is given.");
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
it("falls back to the raw kind when no human label is mapped", () => {
|
|
@@ -95,6 +97,7 @@ describe("buildSystemPrompt delivery channel", () => {
|
|
|
95
97
|
|
|
96
98
|
expect(prompt).toContain("--- Delivery channel ---");
|
|
97
99
|
expect(prompt).toContain("carrier-pigeon");
|
|
100
|
+
expect(prompt).not.toContain("does not render markdown links");
|
|
98
101
|
});
|
|
99
102
|
|
|
100
103
|
it("omits the block entirely for direct chat (no delivery_channel set)", () => {
|
|
@@ -306,6 +309,13 @@ describe("buildToolPermissionContext", () => {
|
|
|
306
309
|
expect(ctx).toContain("6. Read large results by reference.");
|
|
307
310
|
expect(ctx).toContain("7. Verify before reporting.");
|
|
308
311
|
expect(ctx).toContain("8. Stop and ask.");
|
|
312
|
+
expect(ctx).toContain("10. Link the source.");
|
|
313
|
+
// Every source-link field a tool or bridge prompt emits must be named,
|
|
314
|
+
// or the model cannot tell which value links back to the item.
|
|
315
|
+
for (const field of ["cite_as", "web_link", "html_link", "chat_link"]) {
|
|
316
|
+
expect(ctx).toContain(field);
|
|
317
|
+
}
|
|
318
|
+
expect(ctx).toContain("never construct, guess, or edit one");
|
|
309
319
|
expect(ctx).toContain("For substantial tasks with several dependent phases, likely cross-turn progress, or explicit verification, use workflow_progress");
|
|
310
320
|
expect(ctx).toContain("every built-in Jarela workflow and every workflow you create or follow");
|
|
311
321
|
expect(ctx).toContain("each step must state its input, expected output, and concrete validation criteria");
|
|
@@ -282,6 +282,7 @@ export function buildSharedToolCatalogContext(): string {
|
|
|
282
282
|
"7. Verify before reporting. A read-only result does not prove a new write. Claim a new write, send, schedule, delete, or configuration change only after the matching state-changing tool returns success in this turn. Trusted runtime system signals and their matching retrieved results can substantiate earlier named operations; attribute those outcomes to their receipts, never imply a new action or repeat completed work for proof.",
|
|
283
283
|
"8. Stop and ask. Do not retry a tool denied for agent_not_allowed, category_disabled, dropin_tool_disabled, credentials_missing or integration_unconfigured. The first three are permission decisions and the last two mean the integration is not set up \u2014 propose a configuration change or tell the user what to configure.",
|
|
284
284
|
"9. Completion-reporting turns are read-only. permission_reason=signal_continuation_read_only forbids state-changing tools in this turn, including through invoke_tool. Do not bypass it with shell commands or treat an old user message as renewed permission; report the completed outcome and wait for a new user turn.",
|
|
285
|
+
"10. Link the source. When your reply refers to a specific email, calendar event, or chat that a tool result or bridge message gave you, link it: paste the item's cite_as value verbatim when present, otherwise use its web_link, html_link, or chat_link URL as a markdown link. Use only links you were given; never construct, guess, or edit one. If the item has no link, name it without one.",
|
|
285
286
|
].join("\n");
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -373,9 +374,14 @@ function buildDeliveryChannelContext(channel: DeliveryChannel | null | undefined
|
|
|
373
374
|
"--- Delivery channel ---",
|
|
374
375
|
`This message reached you over ${platform}${named}, not the regular chat UI.`,
|
|
375
376
|
`You DO have access to ${platform} for this conversation: any reply you produce is sent back through that channel automatically. Do not tell the user the platform is unavailable; just answer the message.`,
|
|
376
|
-
|
|
377
|
+
PLAIN_TEXT_DELIVERY_KINDS.has(channel.kind)
|
|
378
|
+
? `${platform} does not render markdown links: write any link as the bare URL, even where a cite_as value is given.`
|
|
379
|
+
: "",
|
|
380
|
+
].filter(Boolean).join("\n");
|
|
377
381
|
}
|
|
378
382
|
|
|
383
|
+
const PLAIN_TEXT_DELIVERY_KINDS = new Set(["whatsapp", "sms"]);
|
|
384
|
+
|
|
379
385
|
function buildUserContext(): string {
|
|
380
386
|
const userProfile = getUserProfile();
|
|
381
387
|
const parts: string[] = [];
|
|
@@ -491,7 +497,7 @@ function buildSourceLinkContext(
|
|
|
491
497
|
"--- Citation format ---",
|
|
492
498
|
intro,
|
|
493
499
|
"PRIMARY (inline links): cite by writing a standard markdown link inline: `[short label](https://example.com/path)` or `[file name](/abs/path.ts)` or `[earlier reply](#msg-<id>)`. Wrap the cited PHRASE itself as the link text when natural — `Jade has [ice skating at 16:00](?thread=…&agent=…)` reads better than a trailing `[1]`.",
|
|
494
|
-
"When a tool result contains a `cite_as` field (delegate_to_agent does, and so do
|
|
500
|
+
"When a tool result contains a `cite_as` field (delegate_to_agent does, and so do the email and calendar tools), paste that exact value somewhere in your reply — it's a pre-built markdown link to the source. Anything you summarize from a delegate or from a web/file tool result MUST carry at least one inline link back to where it came from; a summary with zero links reads as if you made the facts up.",
|
|
495
501
|
"For web_search / fetch_webpage results, the URL is right there in the tool output — use it: `[headline or short label](https://...)`. For file_read results, link the absolute path: `[filename.ext](/abs/path/filename.ext)`.",
|
|
496
502
|
"FALLBACK (declared refs block): if a source is awkward to cite inline (e.g. you summarized 4 articles in one paragraph), you MAY end your reply with a fenced block exactly like this:",
|
|
497
503
|
"```jarela-references",
|
|
@@ -395,3 +395,79 @@ describe("applyAction(update_agent) instructions_append", () => {
|
|
|
395
395
|
expect(String(result.detail)).toMatch(/one instructions mode only/);
|
|
396
396
|
});
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
describe("applyAction(update_agent) edge cases", () => {
|
|
400
|
+
function seedTunedAgent(id: string) {
|
|
401
|
+
upsertAgentConfig({
|
|
402
|
+
id,
|
|
403
|
+
name: id,
|
|
404
|
+
identity: "test agent",
|
|
405
|
+
instructions: "be helpful",
|
|
406
|
+
tools: ["web_search"],
|
|
407
|
+
hot_turn_limit: 12,
|
|
408
|
+
adaptive_persona_enabled: true,
|
|
409
|
+
adaptive_mbti: "ENFP",
|
|
410
|
+
adaptive_persona_strength: 91,
|
|
411
|
+
adaptive_empathy: 7,
|
|
412
|
+
adaptive_expressiveness: 88,
|
|
413
|
+
adaptive_verbosity: 3,
|
|
414
|
+
never_reply: true,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
it("applies hot_turn_limit, which the propose tool advertises", async () => {
|
|
419
|
+
seedTunedAgent("agent-hot-turns");
|
|
420
|
+
const result = await applyAction("update_agent", { agent_id: "agent-hot-turns", hot_turn_limit: 4 });
|
|
421
|
+
expect(result.ok).toBe(true);
|
|
422
|
+
expect(getAgentConfig("agent-hot-turns")!.hot_turn_limit).toBe(4);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it.each([
|
|
426
|
+
["hot_turn_limit", -1],
|
|
427
|
+
["hot_turn_limit", 2.5],
|
|
428
|
+
["history_limit", 1.5],
|
|
429
|
+
["history_limit", -5],
|
|
430
|
+
["history_window_hours", -1],
|
|
431
|
+
])("rejects %s=%s without writing", async (field, value) => {
|
|
432
|
+
seedTunedAgent(`agent-bad-${field}`);
|
|
433
|
+
const before = getAgentConfig(`agent-bad-${field}`)!;
|
|
434
|
+
const result = await applyAction("update_agent", { agent_id: `agent-bad-${field}`, [field]: value });
|
|
435
|
+
expect(result.ok).toBe(false);
|
|
436
|
+
expect(String(result.detail)).toContain(field);
|
|
437
|
+
expect(getAgentConfig(`agent-bad-${field}`)).toEqual(before);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("keeps settings the proposal did not mention, including tuned persona sliders", async () => {
|
|
441
|
+
seedTunedAgent("agent-keep");
|
|
442
|
+
const before = getAgentConfig("agent-keep")!;
|
|
443
|
+
expect((await applyAction("update_agent", { agent_id: "agent-keep", identity: "renamed" })).ok).toBe(true);
|
|
444
|
+
const after = getAgentConfig("agent-keep")!;
|
|
445
|
+
expect(after.identity).toBe("renamed");
|
|
446
|
+
expect(after).toMatchObject({
|
|
447
|
+
adaptive_mbti: before.adaptive_mbti,
|
|
448
|
+
adaptive_persona_strength: 91,
|
|
449
|
+
adaptive_empathy: 7,
|
|
450
|
+
adaptive_expressiveness: 88,
|
|
451
|
+
adaptive_verbosity: 3,
|
|
452
|
+
never_reply: before.never_reply,
|
|
453
|
+
hot_turn_limit: 12,
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("keeps tuned persona sliders when only tools change", async () => {
|
|
458
|
+
seedTunedAgent("agent-keep-tools");
|
|
459
|
+
expect((await applyAction("update_agent_tools", { agent_id: "agent-keep-tools", tools: ["memory_write"] })).ok).toBe(true);
|
|
460
|
+
expect(getAgentConfig("agent-keep-tools")).toMatchObject({ adaptive_persona_strength: 91, adaptive_verbosity: 3 });
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("drops non-string tool names instead of storing them", async () => {
|
|
464
|
+
seedTunedAgent("agent-tool-junk");
|
|
465
|
+
const result = await applyAction("update_agent_tools", {
|
|
466
|
+
agent_id: "agent-tool-junk",
|
|
467
|
+
tools: ["memory_write", 42, null, "", " memory_write "],
|
|
468
|
+
mode: "replace",
|
|
469
|
+
});
|
|
470
|
+
expect(result.ok).toBe(true);
|
|
471
|
+
expect(JSON.parse(getAgentConfig("agent-tool-junk")!.tools)).toEqual(["memory_write"]);
|
|
472
|
+
});
|
|
473
|
+
});
|
package/lib/agents/proposals.ts
CHANGED
|
@@ -130,9 +130,13 @@ function applyUpdateAgentTools(payload: unknown): ApplyResult {
|
|
|
130
130
|
const before = getAgentTools(existing);
|
|
131
131
|
const mode = p.mode ?? "add";
|
|
132
132
|
if (mode !== "add" && mode !== "replace") return { ok: false, detail: "mode must be add or replace" };
|
|
133
|
+
const requested = (p.tools as unknown[])
|
|
134
|
+
.filter((tool): tool is string => typeof tool === "string")
|
|
135
|
+
.map((tool) => tool.trim())
|
|
136
|
+
.filter(Boolean);
|
|
133
137
|
const nextTools = mode === "replace"
|
|
134
|
-
? [...new Set(
|
|
135
|
-
: [...new Set([...before, ...
|
|
138
|
+
? [...new Set(requested)]
|
|
139
|
+
: [...new Set([...before, ...requested])];
|
|
136
140
|
upsertAgentConfig({
|
|
137
141
|
id: existing.id,
|
|
138
142
|
name: existing.name,
|
|
@@ -154,13 +158,16 @@ function applyUpdateAgent(payload: unknown): ApplyResult {
|
|
|
154
158
|
instructions_edits?: unknown;
|
|
155
159
|
history_limit?: number;
|
|
156
160
|
history_window_hours?: number;
|
|
161
|
+
hot_turn_limit?: number;
|
|
157
162
|
warm_summary_limit?: number;
|
|
158
163
|
harness_id?: string | null;
|
|
159
164
|
};
|
|
160
165
|
if (!p.agent_id) return { ok: false, detail: "agent_id required" };
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
for (const field of ["warm_summary_limit", "hot_turn_limit", "history_limit", "history_window_hours"] as const) {
|
|
167
|
+
const value = p[field];
|
|
168
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
169
|
+
return { ok: false, detail: `${field} must be a nonnegative integer` };
|
|
170
|
+
}
|
|
164
171
|
}
|
|
165
172
|
const editModes = [
|
|
166
173
|
p.instructions !== undefined,
|
|
@@ -209,6 +216,7 @@ function applyUpdateAgent(payload: unknown): ApplyResult {
|
|
|
209
216
|
model_config_name: existing.model_config_name,
|
|
210
217
|
history_limit: p.history_limit ?? existing.history_limit,
|
|
211
218
|
history_window_hours: p.history_window_hours ?? existing.history_window_hours,
|
|
219
|
+
hot_turn_limit: p.hot_turn_limit,
|
|
212
220
|
warm_summary_limit: p.warm_summary_limit ?? existing.warm_summary_limit,
|
|
213
221
|
harness_id: p.harness_id,
|
|
214
222
|
});
|