@agent-native/core 0.79.6 → 0.79.9
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 +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +0 -93
- package/corpus/core/src/agent/run-manager.ts +9 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
- package/corpus/core/src/client/session-replay.ts +67 -13
- package/corpus/core/src/onboarding/default-steps.ts +3 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
- package/corpus/core/src/server/agent-run-context.ts +204 -0
- package/corpus/core/src/server/credential-provider.ts +20 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
- package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
- package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
- package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
- package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
- package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
- package/corpus/templates/clips/app/routes/record.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
- package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
- package/corpus/templates/clips/desktop/src/app.tsx +85 -18
- package/corpus/templates/clips/desktop/src/styles.css +55 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
- package/corpus/templates/slides/actions/_app-url.ts +1 -1
- package/corpus/templates/slides/changelog/2026-06-26-deck-links-now-use-the-workspace-gateway-when-available-so-g.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +0 -79
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +12 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +15 -8
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +55 -14
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/onboarding/default-steps.js +3 -3
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +69 -154
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/agent-run-context.d.ts +35 -0
- package/dist/server/agent-run-context.d.ts.map +1 -0
- package/dist/server/agent-run-context.js +137 -0
- package/dist/server/agent-run-context.js.map +1 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +15 -4
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/package.json +1 -1
- package/corpus/templates/.retired/README.md +0 -9
- package/corpus/templates/.retired/calls/package.json +0 -8
- package/corpus/templates/.retired/code/package.json +0 -8
- package/corpus/templates/.retired/contracts/package.json +0 -8
- package/corpus/templates/.retired/images/package.json +0 -8
- package/corpus/templates/.retired/issues/package.json +0 -8
- package/corpus/templates/.retired/meeting-notes/package.json +0 -8
- package/corpus/templates/.retired/migration/package.json +0 -8
- package/corpus/templates/.retired/recruiting/package.json +0 -8
- package/corpus/templates/.retired/scheduling/package.json +0 -8
- package/corpus/templates/.retired/visual-plans/package.json +0 -8
- package/corpus/templates/.retired/voice/package.json +0 -8
- package/corpus/templates/.retired/workbench/package.json +0 -8
|
@@ -147,6 +147,12 @@ import {
|
|
|
147
147
|
import { dbExecToolParameters } from "../scripts/db/tool-schemas.js";
|
|
148
148
|
import { getSetting, putSetting } from "../settings/store.js";
|
|
149
149
|
import { discoverAgents } from "./agent-discovery.js";
|
|
150
|
+
import {
|
|
151
|
+
resolveAgentRunOwnerContext,
|
|
152
|
+
runWithAgentRunContext,
|
|
153
|
+
seedBackgroundAgentRunOwnerContext,
|
|
154
|
+
type AgentRunOwnerContext,
|
|
155
|
+
} from "./agent-run-context.js";
|
|
150
156
|
import {
|
|
151
157
|
AGENT_TEAM_PROCESS_RUN_PATH,
|
|
152
158
|
getCurrentDelegationDepth,
|
|
@@ -159,6 +165,7 @@ import {
|
|
|
159
165
|
getBuilderBrowserConnectUrlForOwner,
|
|
160
166
|
resolveBuilderBranchProjectId,
|
|
161
167
|
} from "./builder-browser.js";
|
|
168
|
+
import { captureError } from "./capture-error.js";
|
|
162
169
|
import { captureCliOutput } from "./cli-capture.js";
|
|
163
170
|
import {
|
|
164
171
|
getH3App,
|
|
@@ -871,14 +878,51 @@ export function assembleA2AFinalResponse(
|
|
|
871
878
|
toolResults: readonly A2AToolResultSummary[],
|
|
872
879
|
options: A2AArtifactResponseOptions & { event?: any } = {},
|
|
873
880
|
): { responseText: string; finalText: string } {
|
|
874
|
-
const
|
|
881
|
+
const terminalError = getA2ATerminalErrorEvent(events);
|
|
882
|
+
const responseText = collectFinalResponseTextFromAgentEvents(events, {
|
|
883
|
+
fallbackToPreToolText: !terminalError,
|
|
884
|
+
});
|
|
875
885
|
const finalText = appendA2AArtifactLinks(responseText, [...toolResults], {
|
|
876
886
|
baseUrl: options.baseUrl ?? resolveArtifactBaseUrl(options.event),
|
|
877
887
|
includeReferencedArtifacts: true,
|
|
878
888
|
});
|
|
889
|
+
if (terminalError && !finalText.trim()) {
|
|
890
|
+
throw new Error(formatA2ATerminalError(terminalError));
|
|
891
|
+
}
|
|
879
892
|
return { responseText, finalText };
|
|
880
893
|
}
|
|
881
894
|
|
|
895
|
+
function getA2ATerminalErrorEvent(
|
|
896
|
+
events: readonly AgentChatEvent[],
|
|
897
|
+
): Extract<AgentChatEvent, { type: "error" }> | null {
|
|
898
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
899
|
+
const event = events[i];
|
|
900
|
+
if (event.type === "clear") continue;
|
|
901
|
+
if (event.type === "done") return null;
|
|
902
|
+
if (event.type === "error") return event;
|
|
903
|
+
if (event.type === "auto_continue") {
|
|
904
|
+
return {
|
|
905
|
+
type: "error",
|
|
906
|
+
error: `Agent stopped before finishing (${event.reason}).`,
|
|
907
|
+
errorCode: event.reason,
|
|
908
|
+
recoverable: true,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function formatA2ATerminalError(
|
|
916
|
+
event: Extract<AgentChatEvent, { type: "error" }>,
|
|
917
|
+
): string {
|
|
918
|
+
const parts = [
|
|
919
|
+
event.error || "Agent failed before producing a final response.",
|
|
920
|
+
event.errorCode ? `code: ${event.errorCode}` : "",
|
|
921
|
+
event.details ? `details: ${event.details}` : "",
|
|
922
|
+
].filter(Boolean);
|
|
923
|
+
return parts.join("\n");
|
|
924
|
+
}
|
|
925
|
+
|
|
882
926
|
/**
|
|
883
927
|
* Creates the `get-framework-context` tool. Returns detailed instructions
|
|
884
928
|
* for framework capabilities that are summarized in the compact prompt.
|
|
@@ -3052,6 +3096,8 @@ In Act mode, if the user asks for generated interactive UI in chat, choose the s
|
|
|
3052
3096
|
|
|
3053
3097
|
These are **NOT** source-code changes and do **NOT** go through \`connect-builder\`. Extensions are sandboxed mini-apps — no source files are touched, no PR is opened, no build is required. Saved extensions can be edited later via \`update-extension\`.
|
|
3054
3098
|
|
|
3099
|
+
If the app exposes native actions or instructions for dashboards, reports, analyses, charts, documents, decks, or other domain artifacts, use those app-native actions first. Choose an extension only when the user explicitly asks for an extension/custom mini-app, or when the app's native artifact format cannot faithfully express the requested interaction.
|
|
3100
|
+
|
|
3055
3101
|
Keep \`create-extension\` payloads compact enough to finish quickly. For complex extensions, create a useful working v1 first, then call \`update-extension\` with focused edits for refinements instead of trying to assemble one enormous initial tool input.
|
|
3056
3102
|
|
|
3057
3103
|
Generated UI content can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send(...)/sendToAgentChat(...). It can receive chat inputs through slotContext/window.onSlotContext. Use agentNative.ui.output for passive current values from knobs, sliders, selections, and controls; it writes application state at \`inline-ui:<extensionId>:output\` scoped to the inline extension id returned by \`render-inline-extension\` or \`show-extension-inline\`. When the user later says "use that value", "apply the current setting", or similar, read it with \`readAppState("inline-ui:<id>:output")\` instead of asking them to send it again. Use agentNative.chat.send for visible submit/apply actions that should put a message into chat. Transient extensionData is browser-local and not agent-readable, synced, promoted, or garbage-collected; use application_state/appFetch, appAction, ui.output, or chat.send for anything the agent or app must observe. Use semantic Tailwind classes like bg-background, text-foreground, bg-primary, border-border, and text-muted-foreground so the UI inherits the parent app theme.
|
|
@@ -3079,7 +3125,7 @@ Route by what the request changes, not how it is phrased. Extensions render in t
|
|
|
3079
3125
|
| Ambiguous, satisfiable either way (e.g. "give me an unread view") | \`render-inline-extension\` for chat-only, \`create-extension\` for reusable |
|
|
3080
3126
|
</routing>
|
|
3081
3127
|
|
|
3082
|
-
Worked examples: "a widget showing unread emails grouped by sender", "a
|
|
3128
|
+
Worked examples: "a widget showing unread emails grouped by sender", "a tracker for my newsletter subscriptions", "a custom kanban board with drag-and-drop rules the app does not have" → \`create-extension\`. "Add an Unread tab to the left navigation", "make the subject lines wrap", "change the inbox grouping logic", "add a field to the compose form" → \`connect-builder\`.`
|
|
3083
3129
|
: `### Extensions Disabled
|
|
3084
3130
|
|
|
3085
3131
|
Extension creation and management tools are disabled for this app. Do not claim you can create, edit, hide, or delete Agent-Native extensions unless the template exposes its own typed action for that workflow. For requests that would otherwise be handled as an extension/widget/dashboard/calculator mini-app, explain that this app has disabled extension tools and use the app's available actions instead.`;
|
|
@@ -3088,6 +3134,8 @@ Extension creation and management tools are disabled for this app. Do not claim
|
|
|
3088
3134
|
|
|
3089
3135
|
In Act mode, if the user asks for generated interactive UI in chat, call \`render-inline-extension\` for one-time inline controls/knobs/calculators/visualizers that do not need saving. If the user asks for an **extension**, **widget**, **dashboard**, **calculator**, or **mini-app** that should be reusable or saved, call \`create-extension\` with a self-contained Alpine.js HTML body. To load a saved extension inline, call \`show-extension-inline\`. These are NOT code changes — extensions are sandboxed mini-apps. Do not preface with "let me build…" — just call the right extension action.
|
|
3090
3136
|
|
|
3137
|
+
Use app-native artifact actions first when they exist for dashboards, reports, analyses, charts, documents, decks, or similar domain artifacts. Pick \`create-extension\` only for explicit extension/custom mini-app requests or for behavior the native artifact format cannot support.
|
|
3138
|
+
|
|
3091
3139
|
Keep the first \`create-extension\` call compact and working. If the request is complex, create the v1 first and then refine with focused \`update-extension\` edits.
|
|
3092
3140
|
|
|
3093
3141
|
Generated UI can read chat inputs from slotContext/window.onSlotContext, see/update app state through appFetch/appAction, use extensionData, record passive current values through agentNative.ui.output(value, opts?), and send visible results through agentNative.chat.send(...) or sendToAgentChat(...). ui.output writes \`inline-ui:<extensionId>:output\` in application state; when the user asks to use the current slider/selection/value, read \`readAppState("inline-ui:<id>:output")\`. Transient extensionData is browser-local only, so do not rely on it for values the agent or app must observe. Use semantic Tailwind theme classes.
|
|
@@ -4994,45 +5042,13 @@ export function createAgentChatPlugin(
|
|
|
4994
5042
|
});
|
|
4995
5043
|
}
|
|
4996
5044
|
|
|
4997
|
-
type OwnerContext = {
|
|
4998
|
-
owner: string;
|
|
4999
|
-
anonymous: boolean;
|
|
5000
|
-
name?: string;
|
|
5001
|
-
};
|
|
5002
|
-
const OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
|
|
5003
|
-
|
|
5004
5045
|
// Resolve owner from the H3 event's session, with an optional
|
|
5005
5046
|
// template-provided anonymous owner for public read-only surfaces.
|
|
5006
|
-
const resolveOwnerContext = async (
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
return eventContext[OWNER_CONTEXT_KEY];
|
|
5012
|
-
}
|
|
5013
|
-
|
|
5014
|
-
const session = await getSession(event);
|
|
5015
|
-
if (session?.email) {
|
|
5016
|
-
const resolved = {
|
|
5017
|
-
owner: session.email,
|
|
5018
|
-
anonymous: false,
|
|
5019
|
-
name: session.name,
|
|
5020
|
-
};
|
|
5021
|
-
if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
|
|
5022
|
-
return resolved;
|
|
5023
|
-
}
|
|
5024
|
-
|
|
5025
|
-
const anonymousOwner = await options?.anonymousOwner?.(event);
|
|
5026
|
-
if (anonymousOwner) {
|
|
5027
|
-
const resolved = { owner: anonymousOwner, anonymous: true };
|
|
5028
|
-
if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
|
|
5029
|
-
return resolved;
|
|
5030
|
-
}
|
|
5031
|
-
|
|
5032
|
-
const { createError } = await import("h3");
|
|
5033
|
-
throw createError({
|
|
5034
|
-
statusCode: 401,
|
|
5035
|
-
statusMessage: "Unauthenticated",
|
|
5047
|
+
const resolveOwnerContext = async (
|
|
5048
|
+
event: any,
|
|
5049
|
+
): Promise<AgentRunOwnerContext> => {
|
|
5050
|
+
return resolveAgentRunOwnerContext(event, {
|
|
5051
|
+
anonymousOwner: options?.anonymousOwner,
|
|
5036
5052
|
});
|
|
5037
5053
|
};
|
|
5038
5054
|
|
|
@@ -7749,92 +7765,17 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7749
7765
|
// the background worker), so both go through identical context + handler
|
|
7750
7766
|
// selection.
|
|
7751
7767
|
const invokeAgentChatHandler = async (event: any) => {
|
|
7752
|
-
// Resolve per-request auth context
|
|
7768
|
+
// Resolve per-request auth context.
|
|
7753
7769
|
const ownerContext = await resolveOwnerContext(event);
|
|
7754
|
-
const owner = ownerContext.owner;
|
|
7755
|
-
|
|
7756
|
-
// Resolve org ID: explicit callback > session.orgId from Better Auth
|
|
7757
|
-
// > implicit org membership. Better Auth leaves session.orgId null
|
|
7758
|
-
// until the user explicitly switches orgs, so a fresh signup with
|
|
7759
|
-
// implicit membership (e.g. domain-matched org) would otherwise see
|
|
7760
|
-
// no org-scoped credentials. getOrgContext() does the same DB lookup
|
|
7761
|
-
// the /builder/status endpoint uses to decide "Connected".
|
|
7762
|
-
let resolvedOrgId: string | undefined;
|
|
7763
|
-
if (options?.resolveOrgId) {
|
|
7764
|
-
resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
|
|
7765
|
-
} else {
|
|
7766
|
-
try {
|
|
7767
|
-
const session = await getSession(event);
|
|
7768
|
-
resolvedOrgId = session?.orgId ?? undefined;
|
|
7769
|
-
} catch {
|
|
7770
|
-
// Session not available
|
|
7771
|
-
}
|
|
7772
|
-
if (!resolvedOrgId) {
|
|
7773
|
-
try {
|
|
7774
|
-
const { getOrgContext } = await import("../org/context.js");
|
|
7775
|
-
const ctx = await getOrgContext(event);
|
|
7776
|
-
resolvedOrgId = ctx.orgId ?? undefined;
|
|
7777
|
-
} catch {
|
|
7778
|
-
// org_members table may not exist yet on first boot
|
|
7779
|
-
}
|
|
7780
|
-
}
|
|
7781
|
-
}
|
|
7782
|
-
// Cookieless durable background worker: getSession()/getOrgContext()
|
|
7783
|
-
// resolve from the (absent) session, so org-scoped credential resolution
|
|
7784
|
-
// (the Builder engine) would miss the owner's org and resolveEngine would
|
|
7785
|
-
// fall back to the anthropic default — bailing on a missing key before
|
|
7786
|
-
// the worker claims its run. The owner IS known here (seeded via
|
|
7787
|
-
// OWNER_CONTEXT_KEY from the run's thread), so resolve the org directly
|
|
7788
|
-
// from the owner email, the same active-org logic the foreground session
|
|
7789
|
-
// would yield.
|
|
7790
|
-
if (!resolvedOrgId && owner) {
|
|
7791
|
-
try {
|
|
7792
|
-
const { resolveOrgIdForEmail } = await import("../org/context.js");
|
|
7793
|
-
resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
|
|
7794
|
-
} catch {
|
|
7795
|
-
// org tables unavailable — proceed without org (foreground-equivalent)
|
|
7796
|
-
}
|
|
7797
|
-
}
|
|
7798
|
-
|
|
7799
|
-
// DURABLE OWNER CONTEXT, PART 2: ORG.
|
|
7800
|
-
// The cookieless `_process-run` self-dispatch (and any other background
|
|
7801
|
-
// re-entry) resolves the owner from the run row, but has NO session —
|
|
7802
|
-
// every org resolver above ultimately derives identity from
|
|
7803
|
-
// getSession(event), so they all come back null here. That silently
|
|
7804
|
-
// scopes the agent to `org_id IS NULL` rows only: it sees and writes
|
|
7805
|
-
// resources outside the user's org, while the foreground UI — which
|
|
7806
|
-
// carries the session cookie — resolves the real org. The result is the
|
|
7807
|
-
// observed split where the agent can't list/read the same resources the
|
|
7808
|
-
// UI shows and writes new rows with a null org. Resolve the org from the
|
|
7809
|
-
// now-known owner email (the same active-org / membership logic
|
|
7810
|
-
// getOrgContext uses, minus the session) so the agent and the UI operate
|
|
7811
|
-
// over the same org-scoped data.
|
|
7812
|
-
if (!resolvedOrgId && owner && !ownerContext.anonymous) {
|
|
7813
|
-
try {
|
|
7814
|
-
const { resolveOrgIdForEmail } = await import("../org/context.js");
|
|
7815
|
-
resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
|
|
7816
|
-
} catch {
|
|
7817
|
-
// org tables unavailable (first boot) — leave org unscoped
|
|
7818
|
-
}
|
|
7819
|
-
}
|
|
7820
7770
|
|
|
7821
|
-
|
|
7822
|
-
// tool calls made by the agent (e.g. log-meal with no explicit date)
|
|
7823
|
-
// resolve "today" in the user's local timezone instead of server UTC.
|
|
7824
|
-
const tzRaw = getHeader(event, "x-user-timezone");
|
|
7825
|
-
const timezone =
|
|
7826
|
-
typeof tzRaw === "string" &&
|
|
7827
|
-
tzRaw.trim().length > 0 &&
|
|
7828
|
-
tzRaw.trim().length < 64
|
|
7829
|
-
? tzRaw.trim()
|
|
7830
|
-
: undefined;
|
|
7831
|
-
|
|
7832
|
-
return runWithRequestContext(
|
|
7771
|
+
return runWithAgentRunContext(
|
|
7833
7772
|
{
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7773
|
+
event,
|
|
7774
|
+
ownerContext,
|
|
7775
|
+
resolveOrgId: options?.resolveOrgId,
|
|
7776
|
+
isBackgroundWorker: Boolean(
|
|
7777
|
+
(event as any).context?.__agentChatBackgroundBody,
|
|
7778
|
+
),
|
|
7838
7779
|
},
|
|
7839
7780
|
() => {
|
|
7840
7781
|
// App-rendered chat can't host direct code edits — HMR/full
|
|
@@ -7949,50 +7890,29 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7949
7890
|
(event as any).context = (event as any).context ?? {};
|
|
7950
7891
|
(event as any).context.__agentChatBackgroundBody = prepared.body;
|
|
7951
7892
|
|
|
7952
|
-
//
|
|
7953
|
-
// the
|
|
7954
|
-
//
|
|
7955
|
-
//
|
|
7956
|
-
//
|
|
7957
|
-
|
|
7958
|
-
// circuit-breaker's inline recovery). The run's chat thread already
|
|
7959
|
-
// records the authenticated owner (written by the foreground under the
|
|
7960
|
-
// signed-in user), so resolve the owner from the run row — NOT from the
|
|
7961
|
-
// request body, which the HMAC does not cover and a caller could forge —
|
|
7962
|
-
// and pre-seed the owner context the handler reads (resolveOwnerContext
|
|
7963
|
-
// returns it before attempting any session lookup).
|
|
7964
|
-
let backgroundOwner: string | null = null;
|
|
7965
|
-
try {
|
|
7966
|
-
const { getRunOwnerEmail } = await import("../agent/run-store.js");
|
|
7967
|
-
backgroundOwner = await getRunOwnerEmail(prepared.runId);
|
|
7968
|
-
if (backgroundOwner) {
|
|
7969
|
-
(event as any).context[OWNER_CONTEXT_KEY] = {
|
|
7970
|
-
owner: backgroundOwner,
|
|
7971
|
-
anonymous: false,
|
|
7972
|
-
};
|
|
7973
|
-
}
|
|
7974
|
-
} catch {
|
|
7975
|
-
// Best-effort: if the lookup fails, resolveOwnerContext runs as before
|
|
7976
|
-
// and throws its normal 401, surfaced via the route_threw diagnostic.
|
|
7977
|
-
}
|
|
7893
|
+
// Durable owner context: this self-dispatch is cookieless (HMAC-only).
|
|
7894
|
+
// Resolve the owner from the persisted run row, never the request body,
|
|
7895
|
+
// then invoke the normal handler. The shared agent-run context helper
|
|
7896
|
+
// expands that owner into the same user/org AsyncLocalStorage context the
|
|
7897
|
+
// foreground request uses, so credential and data scoping stay aligned.
|
|
7898
|
+
await seedBackgroundAgentRunOwnerContext(event, prepared.runId);
|
|
7978
7899
|
|
|
7979
7900
|
try {
|
|
7980
|
-
|
|
7981
|
-
// Seeding OWNER_CONTEXT_KEY (above) only feeds getOwnerFromEvent; the
|
|
7982
|
-
// engine-resolution path (detectEngineFromUserSecrets) and other
|
|
7983
|
-
// owner-scoped reads use getRequestUserEmail()/getRequestOrgId(),
|
|
7984
|
-
// which a cookieless background worker otherwise leaves empty. Without
|
|
7985
|
-
// this, resolveEngine can't see the owner's Builder credential, falls
|
|
7986
|
-
// back to the anthropic default, and — when the owner has no stored
|
|
7987
|
-
// anthropic key and deploy-credential fallback is blocked — bails at
|
|
7988
|
-
// the API-key check BEFORE claiming its run (the analytics durable
|
|
7989
|
-
// worker's "stalls at post_model, never claims" bug).
|
|
7990
|
-
const invoke = () => invokeAgentChatHandler(event);
|
|
7991
|
-
return await (backgroundOwner
|
|
7992
|
-
? runWithRequestContext({ userEmail: backgroundOwner }, invoke)
|
|
7993
|
-
: invoke());
|
|
7901
|
+
return await invokeAgentChatHandler(event);
|
|
7994
7902
|
} catch (err: any) {
|
|
7995
7903
|
console.error("[agent-chat] _process-run failed:", err);
|
|
7904
|
+
captureError(err, {
|
|
7905
|
+
route: AGENT_CHAT_PROCESS_RUN_PATH,
|
|
7906
|
+
method: getMethod(event),
|
|
7907
|
+
userAgent: getHeader(event, "user-agent"),
|
|
7908
|
+
tags: {
|
|
7909
|
+
source: "agent-chat-bg-worker",
|
|
7910
|
+
phase: "process-run",
|
|
7911
|
+
},
|
|
7912
|
+
extra: {
|
|
7913
|
+
runId: prepared.runId,
|
|
7914
|
+
},
|
|
7915
|
+
});
|
|
7996
7916
|
// DIAGNOSTIC: the worker invocation threw at the route boundary —
|
|
7997
7917
|
// record the message so the failure cause is readable client-side.
|
|
7998
7918
|
if (diag) {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createError, getHeader, type H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import { resolveOrgIdForEmail, getOrgContext } from "../org/context.js";
|
|
4
|
+
import { getSession } from "./auth.js";
|
|
5
|
+
import {
|
|
6
|
+
runWithRequestContext,
|
|
7
|
+
type RequestContext,
|
|
8
|
+
} from "./request-context.js";
|
|
9
|
+
|
|
10
|
+
export type AgentRunOwnerContext = {
|
|
11
|
+
owner: string;
|
|
12
|
+
anonymous: boolean;
|
|
13
|
+
name?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const AGENT_RUN_OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
|
|
17
|
+
|
|
18
|
+
type EventWithAgentRunContext = H3Event & {
|
|
19
|
+
context?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AnonymousOwnerResolver = (
|
|
23
|
+
event: H3Event,
|
|
24
|
+
) => string | null | Promise<string | null>;
|
|
25
|
+
|
|
26
|
+
type OrgIdResolver = (
|
|
27
|
+
event: H3Event,
|
|
28
|
+
) => string | null | undefined | Promise<string | null | undefined>;
|
|
29
|
+
|
|
30
|
+
function eventContext(
|
|
31
|
+
event: EventWithAgentRunContext,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
event.context = event.context ?? {};
|
|
34
|
+
return event.context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeId(value: string | null | undefined): string | undefined {
|
|
38
|
+
return typeof value === "string" && value.trim().length > 0
|
|
39
|
+
? value.trim()
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readHeaderValue(event: any, name: string): unknown {
|
|
44
|
+
try {
|
|
45
|
+
const value = getHeader(event, name);
|
|
46
|
+
if (value !== undefined && value !== null) return value;
|
|
47
|
+
} catch {
|
|
48
|
+
// Unit tests and a few framework internals pass lightweight event shims.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const headers = event?.headers;
|
|
52
|
+
if (headers && typeof headers.get === "function") {
|
|
53
|
+
return headers.get(name) ?? headers.get(name.toLowerCase()) ?? undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const reqHeaders = event?.node?.req?.headers ?? event?.req?.headers;
|
|
57
|
+
if (reqHeaders && typeof reqHeaders === "object") {
|
|
58
|
+
return reqHeaders[name] ?? reqHeaders[name.toLowerCase()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readAgentRunTimezone(event: H3Event): string | undefined {
|
|
65
|
+
const raw = readHeaderValue(event, "x-user-timezone");
|
|
66
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
67
|
+
return typeof value === "string" &&
|
|
68
|
+
value.trim().length > 0 &&
|
|
69
|
+
value.trim().length < 64
|
|
70
|
+
? value.trim()
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function seedAgentRunOwnerContext(
|
|
75
|
+
event: H3Event,
|
|
76
|
+
ownerContext: AgentRunOwnerContext,
|
|
77
|
+
): AgentRunOwnerContext {
|
|
78
|
+
eventContext(event as EventWithAgentRunContext)[AGENT_RUN_OWNER_CONTEXT_KEY] =
|
|
79
|
+
ownerContext;
|
|
80
|
+
return ownerContext;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function seedBackgroundAgentRunOwnerContext(
|
|
84
|
+
event: H3Event,
|
|
85
|
+
runId: string,
|
|
86
|
+
): Promise<AgentRunOwnerContext | null> {
|
|
87
|
+
try {
|
|
88
|
+
const { getRunOwnerEmail } = await import("../agent/run-store.js");
|
|
89
|
+
const owner = await getRunOwnerEmail(runId);
|
|
90
|
+
if (!owner) return null;
|
|
91
|
+
return seedAgentRunOwnerContext(event, { owner, anonymous: false });
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function resolveAgentRunOwnerContext(
|
|
98
|
+
event: H3Event,
|
|
99
|
+
options: { anonymousOwner?: AnonymousOwnerResolver } = {},
|
|
100
|
+
): Promise<AgentRunOwnerContext> {
|
|
101
|
+
const ctx = eventContext(event as EventWithAgentRunContext);
|
|
102
|
+
const seeded = ctx[AGENT_RUN_OWNER_CONTEXT_KEY] as
|
|
103
|
+
| AgentRunOwnerContext
|
|
104
|
+
| undefined;
|
|
105
|
+
if (seeded) return seeded;
|
|
106
|
+
|
|
107
|
+
const session = await getSession(event);
|
|
108
|
+
if (session?.email) {
|
|
109
|
+
return seedAgentRunOwnerContext(event, {
|
|
110
|
+
owner: session.email,
|
|
111
|
+
anonymous: false,
|
|
112
|
+
name: session.name,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const anonymousOwner = await options.anonymousOwner?.(event);
|
|
117
|
+
if (anonymousOwner) {
|
|
118
|
+
return seedAgentRunOwnerContext(event, {
|
|
119
|
+
owner: anonymousOwner,
|
|
120
|
+
anonymous: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw createError({
|
|
125
|
+
statusCode: 401,
|
|
126
|
+
statusMessage: "Unauthenticated",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function resolveAgentRunOrgId(options: {
|
|
131
|
+
event: H3Event;
|
|
132
|
+
ownerContext: AgentRunOwnerContext;
|
|
133
|
+
resolveOrgId?: OrgIdResolver;
|
|
134
|
+
}): Promise<string | undefined> {
|
|
135
|
+
let resolvedOrgId: string | undefined;
|
|
136
|
+
|
|
137
|
+
if (options.resolveOrgId) {
|
|
138
|
+
resolvedOrgId = normalizeId(await options.resolveOrgId(options.event));
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
const session = await getSession(options.event);
|
|
142
|
+
resolvedOrgId = normalizeId(session?.orgId);
|
|
143
|
+
} catch {
|
|
144
|
+
// Session not available.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!resolvedOrgId) {
|
|
148
|
+
try {
|
|
149
|
+
const orgContext = await getOrgContext(options.event);
|
|
150
|
+
resolvedOrgId = normalizeId(orgContext.orgId);
|
|
151
|
+
} catch {
|
|
152
|
+
// Org tables may not exist yet on first boot.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
!resolvedOrgId &&
|
|
159
|
+
options.ownerContext.owner &&
|
|
160
|
+
!options.ownerContext.anonymous
|
|
161
|
+
) {
|
|
162
|
+
try {
|
|
163
|
+
resolvedOrgId = normalizeId(
|
|
164
|
+
await resolveOrgIdForEmail(options.ownerContext.owner),
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
// Org tables may not exist yet on first boot.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return resolvedOrgId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function resolveAgentRunRequestContext(options: {
|
|
175
|
+
event: H3Event;
|
|
176
|
+
ownerContext: AgentRunOwnerContext;
|
|
177
|
+
resolveOrgId?: OrgIdResolver;
|
|
178
|
+
isBackgroundWorker?: boolean;
|
|
179
|
+
}): Promise<RequestContext> {
|
|
180
|
+
const orgId = await resolveAgentRunOrgId(options);
|
|
181
|
+
const timezone = readAgentRunTimezone(options.event);
|
|
182
|
+
return {
|
|
183
|
+
userEmail: options.ownerContext.owner,
|
|
184
|
+
userName: options.ownerContext.name,
|
|
185
|
+
orgId,
|
|
186
|
+
timezone,
|
|
187
|
+
...(options.isBackgroundWorker
|
|
188
|
+
? { run: { isBackgroundWorker: true } }
|
|
189
|
+
: {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function runWithAgentRunContext<T>(
|
|
194
|
+
options: {
|
|
195
|
+
event: H3Event;
|
|
196
|
+
ownerContext: AgentRunOwnerContext;
|
|
197
|
+
resolveOrgId?: OrgIdResolver;
|
|
198
|
+
isBackgroundWorker?: boolean;
|
|
199
|
+
},
|
|
200
|
+
fn: () => T | Promise<T>,
|
|
201
|
+
): Promise<T> {
|
|
202
|
+
const requestContext = await resolveAgentRunRequestContext(options);
|
|
203
|
+
return await runWithRequestContext(requestContext, fn);
|
|
204
|
+
}
|
|
@@ -80,7 +80,7 @@ export function readDeployCredentialEnv(key: string): string | undefined {
|
|
|
80
80
|
* key does not silently power another tenant's chat.
|
|
81
81
|
*/
|
|
82
82
|
export function isDeployCredentialFallbackAllowed(): boolean {
|
|
83
|
-
if (
|
|
83
|
+
if (!isProductionLikeRuntime()) return true;
|
|
84
84
|
return isLocalDatabase();
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -121,6 +121,22 @@ function isHostedWorkspaceRuntime(): boolean {
|
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
function isProductionLikeRuntime(): boolean {
|
|
125
|
+
return (
|
|
126
|
+
process.env.NODE_ENV === "production" ||
|
|
127
|
+
/^(1|true)$/i.test(process.env.NETLIFY ?? "") ||
|
|
128
|
+
/^(1|true)$/i.test(process.env.VERCEL ?? "") ||
|
|
129
|
+
/^(1|true)$/i.test(process.env.CF_PAGES ?? "") ||
|
|
130
|
+
Boolean(
|
|
131
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
|
132
|
+
process.env.AWS_EXECUTION_ENV ||
|
|
133
|
+
process.env.FUNCTIONS_WORKER_RUNTIME ||
|
|
134
|
+
process.env.K_SERVICE ||
|
|
135
|
+
process.env.RENDER,
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
/**
|
|
125
141
|
* Whether deployment-level Builder env keys may back the current request.
|
|
126
142
|
*
|
|
@@ -143,18 +159,18 @@ function canUseBuilderDeployCredentialFallbackForRequest(): boolean {
|
|
|
143
159
|
const email = getRequestUserEmail();
|
|
144
160
|
if (!email) return true;
|
|
145
161
|
|
|
146
|
-
const
|
|
162
|
+
const isProductionRuntime = isProductionLikeRuntime();
|
|
147
163
|
// Local dogfooding escape hatch: lets the env / root-`.env` Builder key back
|
|
148
164
|
// a signed-in user so running the app locally doesn't require completing the
|
|
149
165
|
// Builder connect flow first. Non-prod only.
|
|
150
166
|
const localDevOptIn =
|
|
151
|
-
!
|
|
167
|
+
!isProductionRuntime &&
|
|
152
168
|
/^(1|true)$/i.test(process.env.AGENT_NATIVE_LOCAL_BUILDER_ENV ?? "");
|
|
153
169
|
|
|
154
170
|
if (isHostedWorkspaceRuntime() && !localDevOptIn) return false;
|
|
155
171
|
|
|
156
172
|
// Deploy fallback is safe in non-prod or on a local/single-tenant database.
|
|
157
|
-
return !
|
|
173
|
+
return !isProductionRuntime || isLocalDatabase();
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
function shouldTraceCredentialResolve(): boolean {
|
|
@@ -593,8 +593,18 @@ function ChartTooltip({
|
|
|
593
593
|
left: number;
|
|
594
594
|
top: number;
|
|
595
595
|
} | null>(null);
|
|
596
|
-
const items =
|
|
597
|
-
|
|
596
|
+
const items = useMemo(
|
|
597
|
+
() =>
|
|
598
|
+
sortTooltipPayloadItems(
|
|
599
|
+
payload?.filter((item) => item.value != null && item.value !== "") ??
|
|
600
|
+
[],
|
|
601
|
+
),
|
|
602
|
+
[payload],
|
|
603
|
+
);
|
|
604
|
+
const portalVisible = portalPosition !== null;
|
|
605
|
+
const clearPortalPosition = useCallback(
|
|
606
|
+
() => setPortalPosition((prev) => (prev === null ? prev : null)),
|
|
607
|
+
[],
|
|
598
608
|
);
|
|
599
609
|
|
|
600
610
|
const labelText =
|
|
@@ -606,7 +616,7 @@ function ChartTooltip({
|
|
|
606
616
|
|
|
607
617
|
useBrowserLayoutEffect(() => {
|
|
608
618
|
if (!active || items.length === 0 || typeof window === "undefined") {
|
|
609
|
-
|
|
619
|
+
clearPortalPosition();
|
|
610
620
|
return;
|
|
611
621
|
}
|
|
612
622
|
|
|
@@ -688,7 +698,7 @@ function ChartTooltip({
|
|
|
688
698
|
window.removeEventListener("resize", schedule);
|
|
689
699
|
window.removeEventListener("scroll", schedule, true);
|
|
690
700
|
};
|
|
691
|
-
});
|
|
701
|
+
}, [active, clearPortalPosition, items.length, portalVisible]);
|
|
692
702
|
|
|
693
703
|
if (!active || items.length === 0) return null;
|
|
694
704
|
|