@agent-native/core 0.172.3 → 0.172.6
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/get-settings.ts +2 -5
- package/corpus/templates/calendar/actions/list-events.ts +21 -80
- package/corpus/templates/calendar/actions/update-settings.ts +11 -16
- package/corpus/templates/calendar/actions/view-screen.ts +25 -10
- package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
- package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
- package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
- package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
- package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
- package/corpus/templates/calendar/shared/settings.ts +15 -9
- package/corpus/templates/calendar/shared/timezone.ts +127 -0
- package/corpus/templates/factory/AGENTS.md +5 -3
- package/corpus/templates/factory/actions/babysit-pull-request.ts +6 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +0 -5
- package/corpus/templates/factory/changelog/2026-08-24-slack-checking-works-for-teammate-created-factories.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-24-teammates-can-edit-and-run-factory-jobs.md +6 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +8 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +1 -9
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -1
- package/corpus/templates/slides/actions/list-decks.ts +79 -5
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
- package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
- package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
- package/corpus/templates/slides/app/pages/Index.tsx +11 -13
- package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
- package/dist/agent/production-agent.js +5 -1
- package/dist/agent/types.d.ts +5 -0
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +20 -0
- package/dist/client/settings/SettingsPanel.js +80 -33
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/run-now.js +2 -2
- package/dist/observability/posthog-ai.d.ts +36 -11
- package/dist/observability/posthog-ai.js +99 -30
- package/dist/observability/routes.js +9 -2
- package/dist/observability/traces.js +330 -142
- package/dist/tracking/providers.js +9 -3
- package/dist/vite/client.js +5 -2
- package/docs/content/docs-components.mdx +20 -1
- package/docs/content/observability.mdx +27 -10
- package/docs/content/template-calendar-agent.mdx +63 -26
- package/docs/content/template-calendar-developers.mdx +165 -40
- package/docs/content/template-calendar-features.mdx +276 -0
- package/docs/content/template-calendar-integrations.mdx +78 -0
- package/docs/content/template-calendar.mdx +56 -123
- package/package.json +1 -1
- package/docs/content/template-calendar-booking-links.mdx +0 -73
- package/docs/content/template-calendar-scheduling.mdx +0 -128
|
@@ -12,6 +12,7 @@ import { AgentWorkspaceContent } from "../agent-page/AgentWorkspaceContent.js";
|
|
|
12
12
|
import { AGENT_PROVIDER_CATALOG, getAgentProviderOption, providerIdForEngine, } from "../agent-provider-catalog.js";
|
|
13
13
|
import { agentNativePath } from "../api-path.js";
|
|
14
14
|
import { BuilderBMark } from "../builder-mark.js";
|
|
15
|
+
import { fetchAgentEngineStatus, fetchEnvironmentStatus, } from "../client-status-requests.js";
|
|
15
16
|
import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js";
|
|
16
17
|
import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js";
|
|
17
18
|
import { useT } from "../i18n.js";
|
|
@@ -332,7 +333,7 @@ const PROVIDER_DOCS = {
|
|
|
332
333
|
"ai-sdk:mistral": "https://console.mistral.ai/api-keys/",
|
|
333
334
|
"ai-sdk:cohere": "https://dashboard.cohere.com/api-keys",
|
|
334
335
|
};
|
|
335
|
-
function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, orgName, envManaged, credentialSource, grouped = false, open, onToggle, }) {
|
|
336
|
+
function LLMSectionInner({ builderFlow, builderLoading, builderStatusAvailable, connectUrl, connected, orgName, envManaged, credentialSource, grouped = false, open, onToggle, }) {
|
|
336
337
|
const isPage = useSettingsSurface() === "page";
|
|
337
338
|
const t = useT();
|
|
338
339
|
const [envKeys, setEnvKeys] = useState([]);
|
|
@@ -354,24 +355,40 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
354
355
|
const [testResult, setTestResult] = useState(null);
|
|
355
356
|
const [settingsStatus, setSettingsStatus] = useState(null);
|
|
356
357
|
const [disconnectError, setDisconnectError] = useState(null);
|
|
357
|
-
const [
|
|
358
|
+
const [envProbeAvailable, setEnvProbeAvailable] = useState(false);
|
|
358
359
|
const [enginesLoaded, setEnginesLoaded] = useState(false);
|
|
359
|
-
const [
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
360
|
+
const [statusProbeAvailable, setStatusProbeAvailable] = useState(false);
|
|
361
|
+
const probeGenerationRef = useRef({ env: 0, status: 0 });
|
|
362
|
+
const initialLoading = !enginesLoaded || !!builderLoading;
|
|
363
|
+
const refreshEnvKeys = useCallback(() => {
|
|
364
|
+
const generation = ++probeGenerationRef.current.env;
|
|
365
|
+
setEnvProbeAvailable(false);
|
|
366
|
+
setEnvKeys([]);
|
|
367
|
+
void fetchEnvironmentStatus().then((result) => {
|
|
368
|
+
if (generation !== probeGenerationRef.current.env)
|
|
369
|
+
return;
|
|
370
|
+
if (result.state !== "available" || !Array.isArray(result.value)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
setEnvKeys(result.value);
|
|
374
|
+
setEnvProbeAvailable(true);
|
|
375
|
+
});
|
|
376
|
+
}, []);
|
|
368
377
|
const notifyConfigChanged = useCallback(() => {
|
|
369
378
|
window.dispatchEvent(new CustomEvent("agent-engine:configured-changed"));
|
|
370
379
|
}, []);
|
|
371
380
|
const refreshSettingsStatus = useCallback(() => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
381
|
+
const generation = ++probeGenerationRef.current.status;
|
|
382
|
+
setStatusProbeAvailable(false);
|
|
383
|
+
setSettingsStatus(null);
|
|
384
|
+
setBaseUrlConfigured(false);
|
|
385
|
+
void fetchAgentEngineStatus()
|
|
386
|
+
.then((result) => {
|
|
387
|
+
if (generation !== probeGenerationRef.current.status)
|
|
388
|
+
return;
|
|
389
|
+
if (result.state !== "available")
|
|
390
|
+
return;
|
|
391
|
+
const data = result.value;
|
|
375
392
|
setBaseUrlConfigured(Boolean(data?.openAiBaseUrlConfigured));
|
|
376
393
|
if (data?.configured &&
|
|
377
394
|
typeof data.engine === "string" &&
|
|
@@ -387,13 +404,34 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
387
404
|
else {
|
|
388
405
|
setSettingsStatus(null);
|
|
389
406
|
}
|
|
407
|
+
setStatusProbeAvailable(true);
|
|
390
408
|
})
|
|
391
|
-
.catch(() => { })
|
|
392
|
-
.finally(() => setStatusLoaded(true));
|
|
409
|
+
.catch(() => { });
|
|
393
410
|
}, []);
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
refreshEnvKeys();
|
|
413
|
+
}, [refreshEnvKeys]);
|
|
394
414
|
useEffect(() => {
|
|
395
415
|
refreshSettingsStatus();
|
|
396
416
|
}, [refreshSettingsStatus]);
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
const refresh = () => {
|
|
419
|
+
refreshEnvKeys();
|
|
420
|
+
refreshSettingsStatus();
|
|
421
|
+
};
|
|
422
|
+
window.addEventListener("agent-engine:configured-changed", refresh);
|
|
423
|
+
window.addEventListener("focus", refresh);
|
|
424
|
+
const onVisibilityChange = () => {
|
|
425
|
+
if (document.visibilityState === "visible")
|
|
426
|
+
refresh();
|
|
427
|
+
};
|
|
428
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
429
|
+
return () => {
|
|
430
|
+
window.removeEventListener("agent-engine:configured-changed", refresh);
|
|
431
|
+
window.removeEventListener("focus", refresh);
|
|
432
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
433
|
+
};
|
|
434
|
+
}, [refreshEnvKeys, refreshSettingsStatus]);
|
|
397
435
|
useEffect(() => {
|
|
398
436
|
callAction("manage-agent-engine", { action: "list" })
|
|
399
437
|
.then((data) => {
|
|
@@ -441,14 +479,21 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
441
479
|
}
|
|
442
480
|
return configured;
|
|
443
481
|
}, [envKeys, settingsStatus]);
|
|
444
|
-
const builderConnected = connected || builderFlow.configured;
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
482
|
+
const builderConnected = builderStatusAvailable && (connected || builderFlow.configured);
|
|
483
|
+
const configurationKnown = envProbeAvailable && statusProbeAvailable;
|
|
484
|
+
const builderEngineSelected = selectedEngine === "builder";
|
|
485
|
+
const selectedConfigurationKnown = builderEngineSelected
|
|
486
|
+
? builderStatusAvailable
|
|
487
|
+
: configurationKnown;
|
|
488
|
+
const anyKeyConfigured = (builderEngineSelected && builderConnected) ||
|
|
489
|
+
(!builderEngineSelected &&
|
|
490
|
+
configurationKnown &&
|
|
491
|
+
selectedEnginePackageInstalled &&
|
|
492
|
+
(envConfigured || settingsConfigured));
|
|
448
493
|
const sourceBadge = computeSourceBadge({
|
|
449
|
-
settingsConfigured,
|
|
450
|
-
settingsStatus,
|
|
451
|
-
envConfigured,
|
|
494
|
+
settingsConfigured: configurationKnown && settingsConfigured,
|
|
495
|
+
settingsStatus: configurationKnown ? settingsStatus : null,
|
|
496
|
+
envConfigured: configurationKnown && envConfigured,
|
|
452
497
|
envVar,
|
|
453
498
|
builderConnected,
|
|
454
499
|
});
|
|
@@ -479,7 +524,6 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
479
524
|
setBaseUrlConfigured(true);
|
|
480
525
|
if (clearBaseUrl)
|
|
481
526
|
setBaseUrlConfigured(false);
|
|
482
|
-
refreshSettingsStatus();
|
|
483
527
|
notifyConfigChanged();
|
|
484
528
|
setTimeout(() => setSaved(false), 2000);
|
|
485
529
|
}
|
|
@@ -496,7 +540,6 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
496
540
|
if (res.ok) {
|
|
497
541
|
setTestResult(null);
|
|
498
542
|
setApplyNote(false);
|
|
499
|
-
refreshSettingsStatus();
|
|
500
543
|
notifyConfigChanged();
|
|
501
544
|
return;
|
|
502
545
|
}
|
|
@@ -560,7 +603,6 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
560
603
|
setCurrentEngine(selectedEngine);
|
|
561
604
|
setCurrentModel(selectedModel);
|
|
562
605
|
setApplyNote(true);
|
|
563
|
-
refreshSettingsStatus();
|
|
564
606
|
notifyConfigChanged();
|
|
565
607
|
setTimeout(() => setApplyNote(false), 4000);
|
|
566
608
|
}
|
|
@@ -568,7 +610,9 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
568
610
|
setApplyError(err instanceof Error ? err.message : String(err));
|
|
569
611
|
}
|
|
570
612
|
};
|
|
571
|
-
return (_jsx(SettingsSection, { id: settingsSectionDomId("llm"), icon: _jsx(IconBrain, { size: 14 }), title: "LLM", required: true, connected: initialLoading
|
|
613
|
+
return (_jsx(SettingsSection, { id: settingsSectionDomId("llm"), icon: _jsx(IconBrain, { size: 14 }), title: "LLM", required: true, connected: initialLoading || !selectedConfigurationKnown
|
|
614
|
+
? undefined
|
|
615
|
+
: anyKeyConfigured, subtitle: isPage
|
|
572
616
|
? undefined
|
|
573
617
|
: t("agentPanel.builderOrOwnKeys", {
|
|
574
618
|
defaultValue: "Choose Builder.io or custom keys.",
|
|
@@ -576,11 +620,11 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
576
620
|
? "flex items-center justify-between gap-4 px-5 py-4 sm:px-6"
|
|
577
621
|
: "flex items-center justify-between gap-2"), children: [isPage && (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "AI provider" }), _jsx("p", { className: "mt-1 text-sm leading-6 text-muted-foreground", children: t("agentPanel.builderOrOwnKeys", {
|
|
578
622
|
defaultValue: "Choose Builder.io or custom keys.",
|
|
579
|
-
}) })] })), _jsxs("div", { className: cn("flex flex-wrap items-center justify-end gap-2"), children: [!anyKeyConfigured && (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected:
|
|
623
|
+
}) })] })), _jsxs("div", { className: cn("flex flex-wrap items-center justify-end gap-2"), children: [selectedConfigurationKnown && !anyKeyConfigured && (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: builderConnected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "llm_settings", trackingFlow: "connect_llm", label: "Connect Builder.io", compact: true })), _jsx(ManualSetupCard, { id: "llm-manual-setup", title: "Custom keys", hint: manualSetupHint, sourceBadge: builderConnected ? undefined : sourceBadge, bare: isPage, popover: true, popoverLabel: settingsConfigured
|
|
580
624
|
? "Manage"
|
|
581
625
|
: t("agentPanel.addOwnKeys", {
|
|
582
626
|
defaultValue: "Custom keys",
|
|
583
|
-
}), summaryContent: anyKeyConfigured ? (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected:
|
|
627
|
+
}), summaryContent: selectedConfigurationKnown && anyKeyConfigured ? (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: builderConnected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "llm_settings", trackingFlow: "connect_llm", label: "Connect Builder.io" })) : undefined, children: _jsxs("div", { className: "space-y-2 mb-1", children: [_jsx(AgentProviderPicker, { value: selectedProvider, configuredProviders: configurationKnown ? configuredProviderIds : undefined, layout: isPage ? "page" : "compact", onChange: (provider) => {
|
|
584
628
|
const option = getAgentProviderOption(provider);
|
|
585
629
|
setSelectedEngine(option.engine);
|
|
586
630
|
setSelectedModel(option.defaultModel);
|
|
@@ -611,7 +655,9 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
|
|
|
611
655
|
: `${envVar} configured`] })) : envVar ? (_jsxs("div", { className: "flex gap-1.5", children: [_jsx("input", { type: "password", value: apiKey, onChange: (e) => setApiKey(e.target.value), onKeyDown: (e) => {
|
|
612
656
|
if (e.key === "Enter")
|
|
613
657
|
handleSave();
|
|
614
|
-
}, placeholder: PROVIDER_ENV_PLACEHOLDERS[envVar] ?? "...", className: cn(textInputClass(isPage), "flex-1"), style: isPage ? CONTROL_STYLE_PAGE : undefined }), _jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleSave, disabled: !providerSettingsChanged || saving, className: pillButtonClass(isPage, "solid"), children: saving ? (_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: isPage ? 14 : 10 })) : ("Save") })] })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { intent: "neutral", emphasis: "outline", onClick: handleTest, disabled: testing
|
|
658
|
+
}, placeholder: PROVIDER_ENV_PLACEHOLDERS[envVar] ?? "...", className: cn(textInputClass(isPage), "flex-1"), style: isPage ? CONTROL_STYLE_PAGE : undefined }), _jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleSave, disabled: !providerSettingsChanged || saving, className: pillButtonClass(isPage, "solid"), children: saving ? (_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: isPage ? 14 : 10 })) : ("Save") })] })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { intent: "neutral", emphasis: "outline", onClick: handleTest, disabled: testing ||
|
|
659
|
+
!selectedConfigurationKnown ||
|
|
660
|
+
!anyKeyConfigured, className: pillButtonClass(isPage, "outline"), children: testing ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" }), "Testing\u2026"] })) : ("Test") }), PROVIDER_DOCS[selectedEngine] ? (_jsxs("a", { href: PROVIDER_DOCS[selectedEngine], target: "_blank", rel: "noopener noreferrer", className: cn(pillButtonClass(isPage, "outline"), "no-underline"), children: ["Get an API key", _jsx(IconExternalLink, { size: isPage ? 14 : 10 })] })) : null, engineChanged && (_jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleApply, className: pillButtonClass(isPage, "solid"), children: "Apply" })), settingsStatus != null && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { intent: "danger", emphasis: "outline", onClick: handleDisconnect, className: cn(pillButtonClass(isPage, "outline"), "ms-auto text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/40"), children: "Disconnect" }) }), _jsx(TooltipContent, { children: "Clear the saved engine \u2014 the app will fall back to the default until you re-apply." })] }))] }), testResult && testResult.ok && (_jsxs("p", { className: cn("flex items-center gap-1 text-primary", isPage ? "text-xs" : "text-[10px]"), children: [_jsx(IconCheck, { size: isPage ? 14 : 10 }), "Test passed \u2014 ", testResult.latencyMs, "ms"] })), testResult && testResult.ok === false && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Test failed: ", testResult.error] })), disconnectError && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Disconnect failed: ", disconnectError] })), applyError && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Apply failed: ", applyError] })), applyNote && (_jsx("p", { className: cn("text-muted-foreground", isPage ? "text-xs" : "text-[10px]"), children: "Changes take effect on next conversation" }))] }) })] })] })) }));
|
|
615
661
|
}
|
|
616
662
|
function friendlyAppName(appId) {
|
|
617
663
|
return appId
|
|
@@ -1056,10 +1102,11 @@ function CapabilityStatusStrip({ isDevMode, builderConnected, builderLoading, bu
|
|
|
1056
1102
|
return (_jsxs("div", { className: "rounded-md border border-border bg-muted/20 px-2.5 py-2", children: [_jsx("div", { className: "mb-1.5 text-[10px] font-medium text-muted-foreground", children: "Available now" }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(CapabilityStatusRow, { label: "App", value: "Chat + actions", active: true }), _jsx(CapabilityStatusRow, { label: "Code", value: codeLabel, active: codeAvailable }), _jsx(CapabilityStatusRow, { label: "Builder", active: builderConnected, value: builderLoading ? (_jsx(Skeleton, { className: "h-3 w-16" })) : builderConnected ? ("Connected") : (_jsx(Button, { type: "button", intent: "neutral", emphasis: "outline", onClick: onOpenLlm, className: "rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground transition-colors hover:bg-accent/40 hover:text-foreground", children: "Connect" })) })] })] }));
|
|
1057
1103
|
}
|
|
1058
1104
|
function SettingsPanelContent({ isDevMode, initialSection, sectionRequestKey, sections = ALL_SETTINGS_SECTIONS, showCapabilityStrip = true, className, surface = "sidebar", builderConnectionOwnedExternally = false, agentAdditionalContent, }) {
|
|
1059
|
-
const { status: builder, loading: builderLoading } = useBuilderStatus({
|
|
1105
|
+
const { status: builder, loading: builderLoading, stale: builderStatusStale, } = useBuilderStatus({
|
|
1060
1106
|
enabled: !builderConnectionOwnedExternally,
|
|
1061
1107
|
});
|
|
1062
1108
|
const connected = builder?.configured ?? false;
|
|
1109
|
+
const builderStatusAvailable = !builderLoading && !builderStatusStale && builder != null;
|
|
1063
1110
|
const connectUrl = builder?.connectUrl;
|
|
1064
1111
|
const orgName = builder?.orgName;
|
|
1065
1112
|
const envManaged = !!builder?.envManaged;
|
|
@@ -1088,7 +1135,7 @@ function SettingsPanelContent({ isDevMode, initialSection, sectionRequestKey, se
|
|
|
1088
1135
|
const isPage = surface === "page";
|
|
1089
1136
|
const isWorkspacePage = isPage && sections.includes("hosting");
|
|
1090
1137
|
return (_jsx(SettingsSurfaceProvider, { surface: surface, children: _jsxs("div", { className: cn(isPage ? "space-y-8" : "flex-1 min-h-0 overflow-y-auto p-3 space-y-2", className), style: isPage ? undefined : { overflowY: "auto" }, children: [showCapabilityStrip && (_jsx(CapabilityStatusStrip, { isDevMode: isDevMode, builderConnected: connected, builderLoading: builderLoading, builderBranchesAvailable: builderBranchesAvailable, onOpenLlm: () => openSettingsSection("llm", true) })), isPage &&
|
|
1091
|
-
["llm", "app-models", "limits", "voice"].some((section) => shouldShowSection(section)) && (_jsxs(SettingsGroup, { title: "Agent", children: [shouldShowSection("llm") && (_jsx(LLMSectionInner, { builderFlow: builderFlow, builderLoading: builderLoading, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, grouped: true, open: openSection === "llm", onToggle: () => toggle("llm") })), shouldShowSection("app-models") && (_jsx(AppModelDefaultsSectionInner, { grouped: true, open: openSection === "app-models", onToggle: () => toggle("app-models") })), shouldShowSection("limits") && (_jsx(AgentLimitsSectionInner, { grouped: true, open: openSection === "limits", onToggle: () => toggle("limits") })), shouldShowSection("voice") && (_jsx(SettingsSection, { id: settingsSectionDomId("voice"), icon: _jsx(IconMicrophone, { size: 14 }), title: "Voice Transcription", subtitle: "Choose how voice input is transcribed.", grouped: true, flat: true, open: openSection === "voice", onToggle: () => toggle("voice"), children: _jsx(VoiceTranscriptionSection, { compact: true }) }))] })), isPage && agentAdditionalContent ? (_jsx(SettingsGroup, { title: "Notifications", children: agentAdditionalContent })) : null, isPage &&
|
|
1138
|
+
["llm", "app-models", "limits", "voice"].some((section) => shouldShowSection(section)) && (_jsxs(SettingsGroup, { title: "Agent", children: [shouldShowSection("llm") && (_jsx(LLMSectionInner, { builderFlow: builderFlow, builderLoading: builderLoading, builderStatusAvailable: builderStatusAvailable, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, grouped: true, open: openSection === "llm", onToggle: () => toggle("llm") })), shouldShowSection("app-models") && (_jsx(AppModelDefaultsSectionInner, { grouped: true, open: openSection === "app-models", onToggle: () => toggle("app-models") })), shouldShowSection("limits") && (_jsx(AgentLimitsSectionInner, { grouped: true, open: openSection === "limits", onToggle: () => toggle("limits") })), shouldShowSection("voice") && (_jsx(SettingsSection, { id: settingsSectionDomId("voice"), icon: _jsx(IconMicrophone, { size: 14 }), title: "Voice Transcription", subtitle: "Choose how voice input is transcribed.", grouped: true, flat: true, open: openSection === "voice", onToggle: () => toggle("voice"), children: _jsx(VoiceTranscriptionSection, { compact: true }) }))] })), isPage && agentAdditionalContent ? (_jsx(SettingsGroup, { title: "Notifications", children: agentAdditionalContent })) : null, isPage &&
|
|
1092
1139
|
["automations", "background"].some((section) => shouldShowSection(section)) && (_jsxs(SettingsGroup, { title: "Agent workflows", children: [shouldShowSection("automations") && (_jsx(SettingsSection, { id: settingsSectionDomId("automations"), icon: _jsx(IconBolt, { size: 14 }), title: "Automations", subtitle: "Scheduled and event-triggered agent tasks.", grouped: true, flat: true, open: openSection === "automations", onToggle: () => toggle("automations"), children: _jsx(SettingsRow, { label: "Automations", description: "Schedule agent tasks or run them from events.", control: _jsxs("a", { href: "/settings/agent/automations", className: "inline-flex items-center gap-1 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground no-underline transition-colors hover:bg-accent/40", children: ["Open automations", _jsx(IconExternalLink, { size: 14 })] }) }) })), shouldShowSection("background") && (_jsx(SettingsSection, { id: settingsSectionDomId("background"), icon: _jsx(IconGitBranch, { size: 14 }), title: "Background Agent", subtitle: "Make code changes from production mode via Builder.", grouped: true, flat: true, connected: connected, open: openSection === "background", onToggle: () => toggle("background"), children: _jsx(SettingsRow, { label: "Background agent", description: "Make code changes from production mode via Builder.", control: _jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "background_agent_settings", trackingFlow: "background_agent", compact: true }) }) }))] })), isWorkspacePage && (_jsxs(SettingsGroup, { title: "Workspace", children: [shouldShowSection("demo-mode") && (_jsx(SettingsRow, { id: settingsSectionDomId("demo-mode"), label: "Demo mode", description: "Use sample data in this browser for presentations.", control: _jsx(DemoModeSection, { compact: true }) })), shouldShowSection("hosting") && (_jsx(SettingsRow, { id: settingsSectionDomId("hosting"), label: "Hosting", description: "Deploy the app to the cloud.", control: _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [!connected && (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "hosting_settings", trackingFlow: "hosting", compact: true })), _jsx(ManualSetupCard, { title: "Set up manually", hint: "Deploy manually to Netlify, Vercel, Cloudflare, or any Nitro-supported target.", docsUrl: docsUrl("deployment", {
|
|
1093
1140
|
campaign: "onboarding",
|
|
1094
1141
|
content: "deployment_settings",
|
|
@@ -1101,7 +1148,7 @@ function SettingsPanelContent({ isDevMode, initialSection, sectionRequestKey, se
|
|
|
1101
1148
|
}), dim: connected, bare: true, popover: true, popoverLabel: "Manage", summaryContent: connected ? (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "file_upload_settings", trackingFlow: "file_upload" })) : undefined, children: _jsx(FileStorageSettingsForm, {}) })] }) })), shouldShowSection("auth") && (_jsx(SettingsRow, { id: settingsSectionDomId("auth"), label: "Authentication", description: "Set up sign-in and access control.", control: _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [!connected && (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "auth_settings", trackingFlow: "auth", compact: true })), _jsx(ManualSetupCard, { title: "Set up manually", hint: "Configure Better Auth and optional Google or GitHub providers.", docsUrl: docsUrl("authentication", {
|
|
1102
1149
|
campaign: "onboarding",
|
|
1103
1150
|
content: "authentication_settings",
|
|
1104
|
-
}), dim: connected, bare: true, popover: true, popoverLabel: "Manage", summaryContent: connected ? (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "auth_settings", trackingFlow: "auth" })) : undefined })] }) }))] })), !isPage && shouldShowSection("llm") && (_jsx(LLMSectionInner, { builderFlow: builderFlow, builderLoading: builderLoading, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, open: openSection === "llm", onToggle: () => toggle("llm") })), !isPage && shouldShowSection("app-models") && (_jsx(AppModelDefaultsSectionInner, { open: openSection === "app-models", onToggle: () => toggle("app-models") })), !isPage && shouldShowSection("limits") && (_jsx(AgentLimitsSectionInner, { open: openSection === "limits", onToggle: () => toggle("limits") })), !isPage && shouldShowSection("voice") && (_jsx(SettingsSection, { id: settingsSectionDomId("voice"), icon: _jsx(IconMicrophone, { size: 14 }), title: "Voice Transcription", subtitle: "How the composer microphone turns your voice into text.", flat: true, open: openSection === "voice", onToggle: () => toggle("voice"), children: _jsx(VoiceTranscriptionSection, {}) })), !isPage && shouldShowSection("demo-mode") && (_jsx(SettingsSection, { id: settingsSectionDomId("demo-mode"), icon: _jsx(IconEyeOff, { size: 14 }), title: "Demo mode", subtitle: "Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, agent integrations, and agent results stay real and access-scoped.", flat: true, open: openSection === "demo-mode", onToggle: () => toggle("demo-mode"), children: _jsx(DemoModeSection, {}) })), !isPage && shouldShowSection("automations") && (_jsx(SettingsSection, { id: settingsSectionDomId("automations"), icon: _jsx(IconBolt, { size: 14 }), title: "Automations", subtitle: "Scheduled and event-triggered agent tasks.", flat: true, open: openSection === "automations", onToggle: () => toggle("automations"), children: _jsx(AutomationsSection, {}) })), shouldShowSection("secrets") && (_jsx(SettingsSection, { id: settingsSectionDomId("secrets"), icon: _jsx(IconKey, { size: 14 }), title: "API keys", subtitle: "Service credentials and automation keys.", flat: true, open: openSection === "secrets", onToggle: () => toggle("secrets"), children: _jsx(SecretsSection, { focusKey: focusSecretKey }) })), !isPage && shouldShowSection("hosting") && (_jsx(SettingsSection, { id: settingsSectionDomId("hosting"), icon: _jsx(IconCloud, { size: 14 }), title: "Hosting", subtitle: "Deploy your app to the cloud.", flat: true, connected: connected, open: openSection === "hosting", onToggle: () => toggle("hosting"), children: _jsxs("div", { className: "space-y-2", children: [_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "hosting_settings", trackingFlow: "hosting" }), _jsx(ManualSetupCard, { hint: "Deploy manually to Netlify, Vercel, Cloudflare, or any Nitro-supported target.", docsUrl: docsUrl("deployment", {
|
|
1151
|
+
}), dim: connected, bare: true, popover: true, popoverLabel: "Manage", summaryContent: connected ? (_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "auth_settings", trackingFlow: "auth" })) : undefined })] }) }))] })), !isPage && shouldShowSection("llm") && (_jsx(LLMSectionInner, { builderFlow: builderFlow, builderLoading: builderLoading, builderStatusAvailable: builderStatusAvailable, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, open: openSection === "llm", onToggle: () => toggle("llm") })), !isPage && shouldShowSection("app-models") && (_jsx(AppModelDefaultsSectionInner, { open: openSection === "app-models", onToggle: () => toggle("app-models") })), !isPage && shouldShowSection("limits") && (_jsx(AgentLimitsSectionInner, { open: openSection === "limits", onToggle: () => toggle("limits") })), !isPage && shouldShowSection("voice") && (_jsx(SettingsSection, { id: settingsSectionDomId("voice"), icon: _jsx(IconMicrophone, { size: 14 }), title: "Voice Transcription", subtitle: "How the composer microphone turns your voice into text.", flat: true, open: openSection === "voice", onToggle: () => toggle("voice"), children: _jsx(VoiceTranscriptionSection, {}) })), !isPage && shouldShowSection("demo-mode") && (_jsx(SettingsSection, { id: settingsSectionDomId("demo-mode"), icon: _jsx(IconEyeOff, { size: 14 }), title: "Demo mode", subtitle: "Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, agent integrations, and agent results stay real and access-scoped.", flat: true, open: openSection === "demo-mode", onToggle: () => toggle("demo-mode"), children: _jsx(DemoModeSection, {}) })), !isPage && shouldShowSection("automations") && (_jsx(SettingsSection, { id: settingsSectionDomId("automations"), icon: _jsx(IconBolt, { size: 14 }), title: "Automations", subtitle: "Scheduled and event-triggered agent tasks.", flat: true, open: openSection === "automations", onToggle: () => toggle("automations"), children: _jsx(AutomationsSection, {}) })), shouldShowSection("secrets") && (_jsx(SettingsSection, { id: settingsSectionDomId("secrets"), icon: _jsx(IconKey, { size: 14 }), title: "API keys", subtitle: "Service credentials and automation keys.", flat: true, open: openSection === "secrets", onToggle: () => toggle("secrets"), children: _jsx(SecretsSection, { focusKey: focusSecretKey }) })), !isPage && shouldShowSection("hosting") && (_jsx(SettingsSection, { id: settingsSectionDomId("hosting"), icon: _jsx(IconCloud, { size: 14 }), title: "Hosting", subtitle: "Deploy your app to the cloud.", flat: true, connected: connected, open: openSection === "hosting", onToggle: () => toggle("hosting"), children: _jsxs("div", { className: "space-y-2", children: [_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "hosting_settings", trackingFlow: "hosting" }), _jsx(ManualSetupCard, { hint: "Deploy manually to Netlify, Vercel, Cloudflare, or any Nitro-supported target.", docsUrl: docsUrl("deployment", {
|
|
1105
1152
|
campaign: "onboarding",
|
|
1106
1153
|
content: "deployment_settings",
|
|
1107
1154
|
}), dim: connected })] }) })), !isPage && shouldShowSection("database") && (_jsx(SettingsSection, { id: settingsSectionDomId("database"), icon: _jsx(IconDatabase, { size: 14 }), title: "Database", subtitle: "Connect a cloud database for persistent storage.", flat: true, connected: connected, open: openSection === "database", onToggle: () => toggle("database"), children: _jsxs("div", { className: "space-y-2", children: [_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "database_settings", trackingFlow: "database" }), _jsx(ManualSetupCard, { hint: "Set DATABASE_URL in your .env to connect Neon, Supabase, Turso, any Postgres/SQLite database, or local PGlite with pglite:./data/pglite.", docsUrl: docsUrl("database", {
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
package/dist/jobs/run-now.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AGENT_CHAT_BACKGROUND_RUN_FIELD, dispatchPathTargetsNetlifyBackgroundFunction, resolveAgentChatProcessRunDispatchPath, } from "../agent/durable-background.js";
|
|
2
|
-
import {
|
|
2
|
+
import { canQueueAutomationRunNow, } from "../automations/service.js";
|
|
3
3
|
import { isLocalDatabase } from "../db/client.js";
|
|
4
4
|
import { organizationResourceOwner, resourceGetByPath, } from "../resources/store.js";
|
|
5
5
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
@@ -77,7 +77,7 @@ export async function queueAutomationRunNow(input) {
|
|
|
77
77
|
statusCode: 404,
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
-
if (!(await
|
|
80
|
+
if (!(await canQueueAutomationRunNow(input, resource, input.scope))) {
|
|
81
81
|
throw Object.assign(new Error("Only the automation's creator or an organization admin can run it."), { statusCode: 403 });
|
|
82
82
|
}
|
|
83
83
|
const { body } = parseJobResource(resource.content);
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* schema, and a name it does not define today it may define tomorrow with
|
|
25
25
|
* different meaning — `input_truncated` / `spans_dropped` cannot collide.
|
|
26
26
|
*
|
|
27
|
-
* Content (`$ai_input` / `$ai_output_choices
|
|
28
|
-
* `$ai_output_state`) is gated on config and always OMITTED
|
|
29
|
-
* Sending `[]` instead would be indistinguishable from a run
|
|
30
|
-
* no messages.
|
|
27
|
+
* Content (a generation's `$ai_input` / `$ai_output_choices`, a span's
|
|
28
|
+
* `$ai_input_state` / `$ai_output_state`) is gated on config and always OMITTED
|
|
29
|
+
* when disabled. Sending `[]` instead would be indistinguishable from a run
|
|
30
|
+
* that genuinely had no messages.
|
|
31
31
|
*
|
|
32
32
|
* @see https://posthog.com/docs/ai-observability/traces
|
|
33
33
|
* @see https://posthog.com/docs/ai-observability/spans
|
|
@@ -42,12 +42,18 @@ export interface AiErrorDetail {
|
|
|
42
42
|
terminal_state?: string;
|
|
43
43
|
retryable?: boolean;
|
|
44
44
|
}
|
|
45
|
+
export declare function resolveAiError(isError: boolean, error: AiErrorDetail | undefined): AiErrorDetail | undefined;
|
|
45
46
|
/**
|
|
46
47
|
* Serialize a content value under a byte ceiling.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* A message list keeps the last user message behind a marker entry naming what
|
|
50
|
+
* was dropped — what was asked is what a trace is opened for, and it stays
|
|
51
|
+
* small. Replacing the whole conversation with a placeholder, which is what
|
|
52
|
+
* this did, left nothing at all.
|
|
53
|
+
*
|
|
54
|
+
* Anything else (a string, an object) still becomes a placeholder: there is no
|
|
55
|
+
* message to keep. Either way `truncated` is true, so a cut payload can never
|
|
56
|
+
* be read as a complete one.
|
|
51
57
|
*/
|
|
52
58
|
export declare function boundAiContent(value: unknown): {
|
|
53
59
|
value: unknown;
|
|
@@ -66,6 +72,10 @@ export interface AiTraceEventInput {
|
|
|
66
72
|
durationMs: number;
|
|
67
73
|
isError: boolean;
|
|
68
74
|
error?: AiErrorDetail;
|
|
75
|
+
/** What KIND of failure this was — a terminal code, a cut-off reason, an
|
|
76
|
+
* engine error code. A classification, never content, so it ships even when
|
|
77
|
+
* the message is withheld. */
|
|
78
|
+
errorType?: string;
|
|
69
79
|
inputTokens?: number;
|
|
70
80
|
outputTokens?: number;
|
|
71
81
|
costUsd?: number;
|
|
@@ -73,11 +83,15 @@ export interface AiTraceEventInput {
|
|
|
73
83
|
/** Browser session id, when the run originated from a page. Links the trace
|
|
74
84
|
* to PostHog session replay. */
|
|
75
85
|
browserSessionId?: string;
|
|
76
|
-
/** Omitted unless `capturePrompts` is on. */
|
|
77
|
-
inputState?: unknown;
|
|
78
|
-
outputState?: unknown;
|
|
79
86
|
extraProperties?: Record<string, unknown>;
|
|
80
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The run's prompt and answer are NOT carried here. Every round-trip emits a
|
|
90
|
+
* generation with its own `$ai_input` / `$ai_output_choices`, so a trace-level
|
|
91
|
+
* copy repeated the first call's prompt and the last call's answer on a second
|
|
92
|
+
* event. The trace owns what only it knows: the run's name, failure, metadata
|
|
93
|
+
* and totals.
|
|
94
|
+
*/
|
|
81
95
|
export declare function emitAiTraceEvent(input: AiTraceEventInput): void;
|
|
82
96
|
export interface AiSpanEventInput {
|
|
83
97
|
runId: string;
|
|
@@ -90,6 +104,9 @@ export interface AiSpanEventInput {
|
|
|
90
104
|
latencySeconds: number;
|
|
91
105
|
isError: boolean;
|
|
92
106
|
error?: AiErrorDetail;
|
|
107
|
+
/** The tool failure's class — `tool_error`, `interrupted`. A classification,
|
|
108
|
+
* never the tool's output, so it ships with `captureToolResults` off. */
|
|
109
|
+
errorType?: string;
|
|
93
110
|
createdAt: number;
|
|
94
111
|
browserSessionId?: string;
|
|
95
112
|
/** Omitted unless `captureToolArgs` / `captureToolResults` are on. */
|
|
@@ -103,8 +120,12 @@ export interface AiFeedbackSurveyInput {
|
|
|
103
120
|
threadId: string | null;
|
|
104
121
|
userId: string | null;
|
|
105
122
|
feedbackType: "thumbs_up" | "thumbs_down" | "category" | "text";
|
|
106
|
-
/** The submitted value:
|
|
123
|
+
/** The submitted value: chosen category or free text. Ignored for thumbs,
|
|
124
|
+
* which answer a rating question and travel as PostHog's choice index. */
|
|
107
125
|
value: string;
|
|
126
|
+
/** Groups the events of one response. A thumbs-down and the follow-up text
|
|
127
|
+
* it opens are two `survey sent` events answering two questions of the SAME
|
|
128
|
+
* response, so they must share this id. */
|
|
108
129
|
submissionId: string;
|
|
109
130
|
model?: string;
|
|
110
131
|
browserSessionId?: string;
|
|
@@ -117,6 +138,10 @@ export interface AiFeedbackSurveyInput {
|
|
|
117
138
|
* nowhere. Returns `false` and emits nothing when no survey id is configured:
|
|
118
139
|
* inventing one would produce events attached to a survey that does not exist.
|
|
119
140
|
*
|
|
141
|
+
* A thumbs vote answers the survey's first question; a category or free-text
|
|
142
|
+
* answer answers the follow-up question after it. Both share the caller's
|
|
143
|
+
* `submissionId`, which is how PostHog joins them into one response.
|
|
144
|
+
*
|
|
120
145
|
* Sent to PostHog ONLY, not through `track()`. The survey response carries the
|
|
121
146
|
* user's free-text feedback verbatim, and configuring a PostHog survey id must
|
|
122
147
|
* not silently start shipping that text to Mixpanel, Amplitude, webhooks, or
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* schema, and a name it does not define today it may define tomorrow with
|
|
25
25
|
* different meaning — `input_truncated` / `spans_dropped` cannot collide.
|
|
26
26
|
*
|
|
27
|
-
* Content (`$ai_input` / `$ai_output_choices
|
|
28
|
-
* `$ai_output_state`) is gated on config and always OMITTED
|
|
29
|
-
* Sending `[]` instead would be indistinguishable from a run
|
|
30
|
-
* no messages.
|
|
27
|
+
* Content (a generation's `$ai_input` / `$ai_output_choices`, a span's
|
|
28
|
+
* `$ai_input_state` / `$ai_output_state`) is gated on config and always OMITTED
|
|
29
|
+
* when disabled. Sending `[]` instead would be indistinguishable from a run
|
|
30
|
+
* that genuinely had no messages.
|
|
31
31
|
*
|
|
32
32
|
* @see https://posthog.com/docs/ai-observability/traces
|
|
33
33
|
* @see https://posthog.com/docs/ai-observability/spans
|
|
@@ -38,6 +38,21 @@ import { boundedText } from "../tracking/redaction.js";
|
|
|
38
38
|
export const MAX_AI_CONTENT_BYTES = 128 * 1024;
|
|
39
39
|
/** Hard ceiling on emitted `$ai_span` events per run. */
|
|
40
40
|
export const MAX_AI_SPANS_PER_RUN = 100;
|
|
41
|
+
/**
|
|
42
|
+
* `$ai_is_error: true` with no `$ai_error` is a dead end: the reader learns
|
|
43
|
+
* something broke and nothing else, which is how a failed tool span reached
|
|
44
|
+
* PostHog saying only "error". Emitters fall back to this rather than shipping
|
|
45
|
+
* the flag alone — a layer that failed without reporting a message is itself
|
|
46
|
+
* worth seeing, and is not the same as a message we chose to withhold.
|
|
47
|
+
*/
|
|
48
|
+
const UNREPORTED_ERROR = {
|
|
49
|
+
message: "failed without a reported error message",
|
|
50
|
+
};
|
|
51
|
+
export function resolveAiError(isError, error) {
|
|
52
|
+
if (!isError)
|
|
53
|
+
return undefined;
|
|
54
|
+
return error ?? UNREPORTED_ERROR;
|
|
55
|
+
}
|
|
41
56
|
function trackAiEvent(name, properties, userId, occurredAt) {
|
|
42
57
|
for (const key of Object.keys(properties)) {
|
|
43
58
|
if (properties[key] === undefined)
|
|
@@ -55,12 +70,24 @@ function trackAiEvent(name, properties, userId, occurredAt) {
|
|
|
55
70
|
// Tracking must never affect the agent run or trace persistence.
|
|
56
71
|
}
|
|
57
72
|
}
|
|
73
|
+
function utf8Bytes(text) {
|
|
74
|
+
return typeof Buffer !== "undefined"
|
|
75
|
+
? Buffer.byteLength(text, "utf8")
|
|
76
|
+
: new TextEncoder().encode(text).length;
|
|
77
|
+
}
|
|
78
|
+
/** Room reserved for the marker entry that reports what was dropped. */
|
|
79
|
+
const OMISSION_MARKER_BYTES = 512;
|
|
58
80
|
/**
|
|
59
81
|
* Serialize a content value under a byte ceiling.
|
|
60
82
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
83
|
+
* A message list keeps the last user message behind a marker entry naming what
|
|
84
|
+
* was dropped — what was asked is what a trace is opened for, and it stays
|
|
85
|
+
* small. Replacing the whole conversation with a placeholder, which is what
|
|
86
|
+
* this did, left nothing at all.
|
|
87
|
+
*
|
|
88
|
+
* Anything else (a string, an object) still becomes a placeholder: there is no
|
|
89
|
+
* message to keep. Either way `truncated` is true, so a cut payload can never
|
|
90
|
+
* be read as a complete one.
|
|
64
91
|
*/
|
|
65
92
|
export function boundAiContent(value) {
|
|
66
93
|
let serialized;
|
|
@@ -72,23 +99,45 @@ export function boundAiContent(value) {
|
|
|
72
99
|
}
|
|
73
100
|
if (serialized === undefined)
|
|
74
101
|
return { value: undefined, truncated: false };
|
|
75
|
-
const bytes =
|
|
76
|
-
? Buffer.byteLength(serialized, "utf8")
|
|
77
|
-
: new TextEncoder().encode(serialized).length;
|
|
102
|
+
const bytes = utf8Bytes(serialized);
|
|
78
103
|
if (bytes <= MAX_AI_CONTENT_BYTES)
|
|
79
104
|
return { value, truncated: false };
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
// What was ASKED is the one thing worth rescuing from an oversized
|
|
107
|
+
// transcript: the rest is context the thread itself still holds, and
|
|
108
|
+
// keeping as much of it as fits just ships the ceiling on every event.
|
|
109
|
+
const lastUser = value.findLast((entry) => !!entry &&
|
|
110
|
+
typeof entry === "object" &&
|
|
111
|
+
entry.role === "user");
|
|
112
|
+
const kept = lastUser !== undefined &&
|
|
113
|
+
utf8Bytes(JSON.stringify(lastUser) ?? "null") <=
|
|
114
|
+
MAX_AI_CONTENT_BYTES - OMISSION_MARKER_BYTES
|
|
115
|
+
? [lastUser]
|
|
116
|
+
: [];
|
|
117
|
+
return {
|
|
118
|
+
value: [
|
|
119
|
+
{
|
|
120
|
+
role: "system",
|
|
121
|
+
content: `[${value.length - kept.length} message(s) omitted: ${bytes} bytes exceeded the ${MAX_AI_CONTENT_BYTES}-byte trace content limit]`,
|
|
122
|
+
},
|
|
123
|
+
...kept,
|
|
124
|
+
],
|
|
125
|
+
truncated: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
80
128
|
return {
|
|
81
129
|
value: `[truncated: ${bytes} bytes exceeded the ${MAX_AI_CONTENT_BYTES}-byte trace content limit]`,
|
|
82
130
|
truncated: true,
|
|
83
131
|
};
|
|
84
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The run's prompt and answer are NOT carried here. Every round-trip emits a
|
|
135
|
+
* generation with its own `$ai_input` / `$ai_output_choices`, so a trace-level
|
|
136
|
+
* copy repeated the first call's prompt and the last call's answer on a second
|
|
137
|
+
* event. The trace owns what only it knows: the run's name, failure, metadata
|
|
138
|
+
* and totals.
|
|
139
|
+
*/
|
|
85
140
|
export function emitAiTraceEvent(input) {
|
|
86
|
-
const inputContent = input.inputState === undefined
|
|
87
|
-
? undefined
|
|
88
|
-
: boundAiContent(input.inputState);
|
|
89
|
-
const outputContent = input.outputState === undefined
|
|
90
|
-
? undefined
|
|
91
|
-
: boundAiContent(input.outputState);
|
|
92
141
|
trackAiEvent("$ai_trace", {
|
|
93
142
|
...input.extraProperties,
|
|
94
143
|
$ai_trace_id: input.runId,
|
|
@@ -97,15 +146,14 @@ export function emitAiTraceEvent(input) {
|
|
|
97
146
|
$ai_model: input.model,
|
|
98
147
|
$ai_provider: input.provider,
|
|
99
148
|
$ai_is_error: input.isError,
|
|
100
|
-
$ai_error: input.error,
|
|
149
|
+
$ai_error: resolveAiError(input.isError, input.error),
|
|
150
|
+
$ai_error_type: input.isError
|
|
151
|
+
? (input.errorType ?? "run_error")
|
|
152
|
+
: undefined,
|
|
101
153
|
duration_ms: Math.round(input.durationMs),
|
|
102
154
|
input_tokens: input.inputTokens,
|
|
103
155
|
output_tokens: input.outputTokens,
|
|
104
156
|
cost_usd: input.costUsd,
|
|
105
|
-
$ai_input_state: inputContent?.value,
|
|
106
|
-
$ai_output_state: outputContent?.value,
|
|
107
|
-
input_truncated: inputContent?.truncated || undefined,
|
|
108
|
-
output_truncated: outputContent?.truncated || undefined,
|
|
109
157
|
$session_id: input.browserSessionId,
|
|
110
158
|
created_at: new Date(input.createdAt).toISOString(),
|
|
111
159
|
}, input.userId, input.createdAt);
|
|
@@ -126,7 +174,10 @@ export function emitAiSpanEvent(input) {
|
|
|
126
174
|
$ai_span_name: input.spanName,
|
|
127
175
|
$ai_latency: input.latencySeconds,
|
|
128
176
|
$ai_is_error: input.isError,
|
|
129
|
-
$ai_error: input.error,
|
|
177
|
+
$ai_error: resolveAiError(input.isError, input.error),
|
|
178
|
+
$ai_error_type: input.isError
|
|
179
|
+
? (input.errorType ?? "tool_error")
|
|
180
|
+
: undefined,
|
|
130
181
|
$ai_input_state: inputContent?.value,
|
|
131
182
|
$ai_output_state: outputContent?.value,
|
|
132
183
|
input_truncated: inputContent?.truncated || undefined,
|
|
@@ -135,6 +186,14 @@ export function emitAiSpanEvent(input) {
|
|
|
135
186
|
created_at: new Date(input.createdAt).toISOString(),
|
|
136
187
|
}, input.userId, input.createdAt);
|
|
137
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* PostHog's thumbs survey is a choice question, and the response is the
|
|
191
|
+
* choice's 1-based index — not a label. Sending "thumbs_up" put a string where
|
|
192
|
+
* the rating belongs, so the vote counted as an unrecognized answer.
|
|
193
|
+
*
|
|
194
|
+
* @see https://posthog.com/docs/ai-observability/user-feedback/manual-event-capture
|
|
195
|
+
*/
|
|
196
|
+
const THUMB_RESPONSE_INDEX = { thumbs_up: 1, thumbs_down: 2 };
|
|
138
197
|
/**
|
|
139
198
|
* Emit PostHog's documented manual feedback event for an LLM trace.
|
|
140
199
|
*
|
|
@@ -143,6 +202,10 @@ export function emitAiSpanEvent(input) {
|
|
|
143
202
|
* nowhere. Returns `false` and emits nothing when no survey id is configured:
|
|
144
203
|
* inventing one would produce events attached to a survey that does not exist.
|
|
145
204
|
*
|
|
205
|
+
* A thumbs vote answers the survey's first question; a category or free-text
|
|
206
|
+
* answer answers the follow-up question after it. Both share the caller's
|
|
207
|
+
* `submissionId`, which is how PostHog joins them into one response.
|
|
208
|
+
*
|
|
146
209
|
* Sent to PostHog ONLY, not through `track()`. The survey response carries the
|
|
147
210
|
* user's free-text feedback verbatim, and configuring a PostHog survey id must
|
|
148
211
|
* not silently start shipping that text to Mixpanel, Amplitude, webhooks, or
|
|
@@ -155,17 +218,23 @@ export function emitAiFeedbackSurveyEvent(input) {
|
|
|
155
218
|
const surveyId = process.env.POSTHOG_AI_FEEDBACK_SURVEY_ID?.trim();
|
|
156
219
|
if (!surveyId)
|
|
157
220
|
return false;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
221
|
+
const thumbResponse = input.feedbackType === "thumbs_up" || input.feedbackType === "thumbs_down"
|
|
222
|
+
? THUMB_RESPONSE_INDEX[input.feedbackType]
|
|
223
|
+
: undefined;
|
|
224
|
+
// `$survey_response` is the survey's first question — the thumbs — and
|
|
225
|
+
// `$survey_response_1` the follow-up asked after a thumbs-down. Putting the
|
|
226
|
+
// free text on the first key instead would file prose where the rating
|
|
227
|
+
// belongs and skew every thumbs metric in the survey.
|
|
228
|
+
const responses = thumbResponse !== undefined
|
|
229
|
+
? { $survey_response: thumbResponse }
|
|
230
|
+
: { $survey_response_1: input.value };
|
|
164
231
|
const properties = {
|
|
165
232
|
$survey_id: surveyId,
|
|
166
|
-
|
|
233
|
+
...responses,
|
|
167
234
|
$survey_submission_id: input.submissionId,
|
|
168
|
-
|
|
235
|
+
// A thumbs-down opens the follow-up question, so the response is not
|
|
236
|
+
// complete until that answer arrives (or the user closes it without one).
|
|
237
|
+
$survey_completed: input.feedbackType !== "thumbs_down",
|
|
169
238
|
$ai_trace_id: input.runId ?? undefined,
|
|
170
239
|
$ai_session_id: input.threadId ?? undefined,
|
|
171
240
|
$ai_model: input.model,
|
|
@@ -216,8 +216,15 @@ export function createObservabilityHandler() {
|
|
|
216
216
|
threadId,
|
|
217
217
|
userId: owner,
|
|
218
218
|
feedbackType,
|
|
219
|
-
value
|
|
220
|
-
|
|
219
|
+
value,
|
|
220
|
+
// One PostHog response per rated message, not per row: a thumbs-down
|
|
221
|
+
// and the free text it opens are two answers to the same survey, and
|
|
222
|
+
// a fresh id per row would file them as two unrelated responses.
|
|
223
|
+
// Falls back to the row id when there is no message to key on, which
|
|
224
|
+
// groups nothing — the honest outcome when nothing can be grouped.
|
|
225
|
+
submissionId: runId && typeof body.messageSeq === "number"
|
|
226
|
+
? `${runId}:${body.messageSeq}`
|
|
227
|
+
: id,
|
|
221
228
|
model,
|
|
222
229
|
browserSessionId: getRequestContext()?.browserSessionId,
|
|
223
230
|
});
|