@agent-native/core 0.222.0 → 0.222.1-nightly-20261029092708

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.
Files changed (43) hide show
  1. package/dist/agent/production-agent.d.ts +2 -0
  2. package/dist/agent/production-agent.js +86 -6
  3. package/dist/agent/run-store.js +3 -2
  4. package/dist/app-config/agent.d.ts +9 -0
  5. package/dist/app-config/agent.js +31 -0
  6. package/dist/app-config/schema.d.ts +9 -0
  7. package/dist/app-config/store.js +48 -5
  8. package/dist/collab/routes.d.ts +1 -1
  9. package/dist/collab/struct-routes.d.ts +1 -1
  10. package/dist/file-upload/actions/upload-image.d.ts +4 -4
  11. package/dist/jobs/run-history.js +3 -2
  12. package/dist/observability/hosted-model-experiment.d.ts +1 -1
  13. package/dist/observability/hosted-model-experiment.js +7 -1
  14. package/dist/observability/question-intake-experiment.d.ts +34 -0
  15. package/dist/observability/question-intake-experiment.js +114 -0
  16. package/dist/observability/traces.d.ts +4 -0
  17. package/dist/observability/traces.js +5 -4
  18. package/dist/observability/tracing.d.ts +1 -0
  19. package/dist/observability/tracing.js +14 -4
  20. package/dist/org/index.d.ts +1 -1
  21. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  22. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  23. package/dist/server/agent-chat/browser-team-tools.js +2 -2
  24. package/dist/server/agent-engine-api-key-route.d.ts +3 -3
  25. package/dist/server/better-auth-instance.js +43 -0
  26. package/dist/server/builder-branch-files.d.ts +3 -1
  27. package/dist/server/builder-branch-files.js +74 -33
  28. package/dist/server/builder-branch-git.d.ts +1 -1
  29. package/dist/server/builder-branch-git.js +2 -0
  30. package/dist/server/builder-browser.d.ts +17 -0
  31. package/dist/server/builder-browser.js +33 -0
  32. package/dist/server/core-routes-plugin.js +42 -8
  33. package/dist/server/index.d.ts +1 -1
  34. package/dist/server/index.js +1 -1
  35. package/dist/server/request-context.d.ts +8 -0
  36. package/dist/server/transcribe-voice.d.ts +1 -1
  37. package/dist/shared/first-run-builder-credits-experiment.d.ts +1 -0
  38. package/dist/shared/first-run-builder-credits-experiment.js +19 -0
  39. package/dist/tracking/index.d.ts +1 -1
  40. package/dist/tracking/index.js +1 -1
  41. package/dist/tracking/registry.d.ts +7 -0
  42. package/dist/tracking/registry.js +84 -4
  43. package/package.json +4 -4
@@ -279,6 +279,7 @@ export declare function isUnavailableActionEntry(entry: ActionEntry | undefined)
279
279
  */
280
280
  export declare function pinAdditionalActionsToTurn(resolved: Record<string, ActionEntry>, turnNames: readonly string[], registered: Record<string, ActionEntry>): Record<string, ActionEntry>;
281
281
  export declare function filterActionsByAllowedNames(actions: Record<string, ActionEntry>, allowedActionNames: readonly string[]): Record<string, ActionEntry>;
282
+ export declare function filterActionsByExcludedNames(actions: Record<string, ActionEntry>, excludedActionNames: readonly string[]): Record<string, ActionEntry>;
282
283
  export declare const PLAN_MODE_SYSTEM_PROMPT = "## Plan Mode Active\n\nYou are in Plan mode. This turn is for research, clarification, and a proposed approach only.\n\nHard rules:\n- Use only read-only tools. Do not edit files, write resources, run mutating bash commands, mutate SQL rows, navigate the UI, send notifications, create jobs, create tools, send messages or tasks to external agents, or change external systems.\n- If a needed detail is unclear, ask a concise clarifying question before proposing a plan.\n- When ready, present a concrete plan with the files/tools you expect to touch, the intended changes, validation steps, and notable risks.\n- Plan mode covers only this message. End with the plan and do not carry it out; the user starts implementation with the Implement button or their next message.";
283
284
  export declare function isPlanModeToolCallAllowed(name: string, input: unknown, entry: ActionEntry): boolean;
284
285
  export declare function createPlanModeActionRegistry(actions: Record<string, ActionEntry>): Record<string, ActionEntry>;
@@ -791,6 +792,7 @@ export declare function createRunSafeBoundaryGuards(input: {
791
792
  beforeModelDispatch: BeforeModelDispatch;
792
793
  beforeWrite: () => Promise<void>;
793
794
  };
795
+ export declare function trackExperimentExposureAtDispatch(beforeModelDispatch: BeforeModelDispatch, trackExposure: () => void): BeforeModelDispatch;
794
796
  export declare function confirmQueuedRunInterrupt(input: {
795
797
  abort: () => Promise<boolean>;
796
798
  defer: () => Promise<unknown>;
@@ -33,7 +33,8 @@ import { isMcpActionResult } from "../mcp-client/app-result.js";
33
33
  import { extractMcpToolResultImages } from "../mcp-client/index.js";
34
34
  import { isMcpToolAllowedForRequest } from "../mcp-client/visibility.js";
35
35
  import { describeSettingsViewForAgent, SETTINGS_VIEW_STATE_KEY, } from "../navigation/settings-redirects.js";
36
- import { isHostedModelExperimentEnabled, resolveHostedModelExperiment, } from "../observability/hosted-model-experiment.js";
36
+ import { isHostedModelExperimentEnabled, resolveHostedModelExperiment, singleNonHostedAssignment, } from "../observability/hosted-model-experiment.js";
37
+ import { InvalidQuestionIntakeExperimentAssignmentError, resolveQuestionIntakeExperimentAssignment, treatmentInstructionsForQuestionIntake, } from "../observability/question-intake-experiment.js";
37
38
  import { isFirstPartyHostedAgentNative, shouldInferSentimentForTurn, } from "../observability/sentiment.js";
38
39
  import { parseServiceIdentityEmail } from "../org/service-identity.js";
39
40
  import { ServicePrincipalRefusedError } from "../org/service-principal-guard.js";
@@ -61,6 +62,7 @@ import { stripDiagnosticSnippets } from "../shared/diagnostic-snippet.js";
61
62
  import { DurableAttachmentReferenceRequiredError, stripInlineBytes, } from "../shared/inline-bytes.js";
62
63
  import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
63
64
  import { SYNTHETIC_TRAFFIC_BETA_E2E, SYNTHETIC_TRAFFIC_HEADER, } from "../shared/test-traffic.js";
65
+ import { trackQuestionIntakeExposure } from "../tracking/index.js";
64
66
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
65
67
  import { agentErrorDetails } from "./action-error-details.js";
66
68
  import { drainAgentWarnings, formatAgentWarningsForToolResult, } from "./action-warnings.js";
@@ -901,6 +903,12 @@ export function filterActionsByAllowedNames(actions, allowedActionNames) {
901
903
  }
902
904
  return filtered;
903
905
  }
906
+ export function filterActionsByExcludedNames(actions, excludedActionNames) {
907
+ if (excludedActionNames.length === 0)
908
+ return actions;
909
+ const excludedNames = new Set(excludedActionNames);
910
+ return Object.fromEntries(Object.entries(actions).filter(([name]) => !excludedNames.has(name)));
911
+ }
904
912
  export const PLAN_MODE_SYSTEM_PROMPT = `## Plan Mode Active
905
913
 
906
914
  You are in Plan mode. This turn is for research, clarification, and a proposed approach only.
@@ -4996,6 +5004,16 @@ export function createRunSafeBoundaryGuards(input) {
4996
5004
  },
4997
5005
  };
4998
5006
  }
5007
+ export function trackExperimentExposureAtDispatch(beforeModelDispatch, trackExposure) {
5008
+ let tracked = false;
5009
+ return async (dispatch) => beforeModelDispatch(() => {
5010
+ if (!tracked) {
5011
+ tracked = true;
5012
+ trackExposure();
5013
+ }
5014
+ return dispatch();
5015
+ });
5016
+ }
4999
5017
  export async function confirmQueuedRunInterrupt(input) {
5000
5018
  if (await input.abort())
5001
5019
  return "aborted";
@@ -9646,7 +9664,7 @@ export function createProductionAgentHandler(options) {
9646
9664
  }
9647
9665
  filtered[name] = entry;
9648
9666
  }
9649
- return filtered;
9667
+ return filterActionsByExcludedNames(filtered, getRequestRunContext()?.excludedActionNames ?? []);
9650
9668
  };
9651
9669
  const getEngineTools = (actions) => actionsToEngineTools(getRequestActions(actions));
9652
9670
  return defineEventHandler(async (event) => {
@@ -9970,6 +9988,40 @@ export function createProductionAgentHandler(options) {
9970
9988
  });
9971
9989
  const ownerEmail = await resolveAgentOwnerEmail(options, event);
9972
9990
  const runRequestContext = getRequestContext();
9991
+ const questionIntakeConfig = getAppConfig().agent.questionIntake;
9992
+ let questionIntakeAssignment = null;
9993
+ try {
9994
+ // raw-sql-ok: the assignment store binds the authenticated user ID as a query argument.
9995
+ questionIntakeAssignment =
9996
+ await resolveQuestionIntakeExperimentAssignment(questionIntakeConfig, runRequestContext?.agentRunAnonymous
9997
+ ? undefined
9998
+ : runRequestContext?.authUserId);
9999
+ }
10000
+ catch (error) {
10001
+ const invalidAssignment = error instanceof InvalidQuestionIntakeExperimentAssignmentError
10002
+ ? error
10003
+ : null;
10004
+ console.error(invalidAssignment
10005
+ ? "[agent-chat] question-intake assignment is invalid"
10006
+ : "[agent-chat] question-intake assignment lookup failed");
10007
+ setResponseStatus(event, invalidAssignment ? 500 : 503);
10008
+ return {
10009
+ error: "The agent run failed.",
10010
+ code: invalidAssignment?.code ?? "question_intake_assignment_unavailable",
10011
+ retryable: invalidAssignment === null,
10012
+ };
10013
+ }
10014
+ if (questionIntakeAssignment) {
10015
+ const runContext = ensureRequestRunContext();
10016
+ if (runContext) {
10017
+ runContext.questionIntakeExperimentAssignment =
10018
+ questionIntakeAssignment;
10019
+ runContext.excludedActionNames =
10020
+ questionIntakeAssignment.variantId === "no_questions"
10021
+ ? (questionIntakeConfig?.questionToolNames ?? [])
10022
+ : [];
10023
+ }
10024
+ }
9973
10025
  const turnInitiator = ownerEmail
9974
10026
  ? {
9975
10027
  email: ownerEmail,
@@ -10858,7 +10910,15 @@ export function createProductionAgentHandler(options) {
10858
10910
  let effectiveModel = model;
10859
10911
  let modelSelectionSource = modelSelection.source;
10860
10912
  const turnUsageLabel = normalizeUsageLabel(body.usageLabel);
10861
- let experimentAssignments = [];
10913
+ let experimentAssignments = questionIntakeAssignment
10914
+ ? [
10915
+ {
10916
+ experimentId: questionIntakeAssignment.experimentId,
10917
+ variantId: questionIntakeAssignment.variantId,
10918
+ },
10919
+ ]
10920
+ : [];
10921
+ let singularExperimentAssignment;
10862
10922
  try {
10863
10923
  // A pinned request model suppresses the experiment's model override, so
10864
10924
  // resolving would assign (and persist) a variant this turn never ran.
@@ -10873,7 +10933,11 @@ export function createProductionAgentHandler(options) {
10873
10933
  : undefined,
10874
10934
  assignments: expConfig.assignments,
10875
10935
  });
10876
- experimentAssignments = experimentSelection.assignments;
10936
+ singularExperimentAssignment = singleNonHostedAssignment(experimentSelection.assignments);
10937
+ experimentAssignments = [
10938
+ ...experimentAssignments,
10939
+ ...experimentSelection.assignments,
10940
+ ];
10877
10941
  if (experimentSelection.model) {
10878
10942
  effectiveModel = normalizeModelForEngine(engine, experimentSelection.model);
10879
10943
  modelSelectionSource = "experiment";
@@ -11013,7 +11077,10 @@ export function createProductionAgentHandler(options) {
11013
11077
  const built = typeof options.systemPrompt === "function"
11014
11078
  ? await options.systemPrompt(event)
11015
11079
  : options.systemPrompt;
11016
- return built;
11080
+ const treatmentInstructions = treatmentInstructionsForQuestionIntake(questionIntakeConfig, questionIntakeAssignment);
11081
+ return treatmentInstructions
11082
+ ? [built, treatmentInstructions].filter(Boolean).join("\n\n")
11083
+ : built;
11017
11084
  }
11018
11085
  catch (error) {
11019
11086
  systemPromptError =
@@ -12254,6 +12321,7 @@ export function createProductionAgentHandler(options) {
12254
12321
  if (backgroundSetupDeadlineExceeded?.()) {
12255
12322
  return { ok: true, skipped: "setup-deadline" };
12256
12323
  }
12324
+ // raw-sql-ok: run callback IDs and prompts reach static queries as bound arguments.
12257
12325
  const startedRun = startRun(runId, effectiveThreadId, async (rawSend, signal, control) => {
12258
12326
  if (backgroundSetupDeadlineExceeded?.())
12259
12327
  return;
@@ -12327,11 +12395,22 @@ export function createProductionAgentHandler(options) {
12327
12395
  }
12328
12396
  return "none";
12329
12397
  };
12330
- const { beforeModelDispatch, beforeWrite } = createRunSafeBoundaryGuards({
12398
+ const { beforeModelDispatch: guardedBeforeModelDispatch, beforeWrite } = createRunSafeBoundaryGuards({
12331
12399
  signal,
12332
12400
  flushEvents: control.flushEvents,
12333
12401
  interruptRequestedAtBoundary: interruptQueuedRunAtBoundary,
12334
12402
  });
12403
+ const exposureAssignment = questionIntakeAssignment && !internalContinuation && !continueOf
12404
+ ? questionIntakeAssignment
12405
+ : null;
12406
+ const beforeModelDispatch = exposureAssignment
12407
+ ? trackExperimentExposureAtDispatch(guardedBeforeModelDispatch, () => trackQuestionIntakeExposure(exposureAssignment, {
12408
+ appId: options.appId ?? "app",
12409
+ ...(threadId === undefined ? {} : { threadId }),
12410
+ turnId: effectiveTurnId,
12411
+ ...(ownerEmail ? { userId: ownerEmail } : {}),
12412
+ }))
12413
+ : guardedBeforeModelDispatch;
12335
12414
  if (selfChainBudget?.skipToBoundary) {
12336
12415
  await recordRunDiagnostic(runId, RUN_DIAG_STAGE.workerSetupStep, `self_chain_budget_exhausted elapsed=${Date.now() - setupT0}ms`).catch(() => { });
12337
12416
  send({ type: "auto_continue", reason: "run_timeout" });
@@ -12809,6 +12888,7 @@ export function createProductionAgentHandler(options) {
12809
12888
  : {}),
12810
12889
  },
12811
12890
  experimentAssignments,
12891
+ singularExperimentAssignment,
12812
12892
  modelSelectionSource,
12813
12893
  sentimentInput: shouldInferSentimentForTurn({
12814
12894
  internalContinuation: Boolean(internalContinuation),
@@ -1,6 +1,6 @@
1
1
  import { MAX_BACKGROUND_RUN_CONTINUATIONS, TURN_RUN_LEDGER_SLACK, } from "../app-config/run-lifecycle-invariants.js";
2
2
  import { isArtifactReceipt, } from "../artifacts/detect.js";
3
- import { getDbExec, getScopedTransactionDbExec } from "../db/client.js";
3
+ import { getDbExec, getScopedDbExec, getScopedTransactionDbExec, } from "../db/client.js";
4
4
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
5
5
  import { defineStore } from "../db/store-registry.js";
6
6
  import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
@@ -1320,7 +1320,8 @@ function earlierTurnProjectionPendingSql() {
1320
1320
  )`;
1321
1321
  }
1322
1322
  export async function claimBackgroundRun(runId) {
1323
- await ensureRunTables();
1323
+ if (getScopedDbExec() === undefined)
1324
+ await ensureRunTables();
1324
1325
  const client = getDbExec();
1325
1326
  const { rowsAffected } = await client.execute({
1326
1327
  sql: `UPDATE agent_runs
@@ -2,6 +2,15 @@ import { z } from "zod";
2
2
  export declare const agentConfig: z.ZodObject<{
3
3
  engine: z.ZodOptional<z.ZodString>;
4
4
  model: z.ZodOptional<z.ZodString>;
5
+ questionIntake: z.ZodOptional<z.ZodObject<{
6
+ enabled: z.ZodDefault<z.ZodBoolean>;
7
+ disabled: z.ZodDefault<z.ZodBoolean>;
8
+ experimentId: z.ZodString;
9
+ questionToolNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
10
+ treatmentInstructions: z.ZodString;
11
+ trackedEventNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
12
+ clientTrackedEventNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strip>>;
5
14
  builtInEngines: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
15
  buildEnginePackages: z.ZodOptional<z.ZodString>;
7
16
  mode: z.ZodOptional<z.ZodString>;
@@ -16,6 +16,37 @@ export const agentConfig = z.object({
16
16
  env: ["AGENT_MODEL"],
17
17
  doc: "Model the agent runs with, when the caller does not pass one.",
18
18
  }),
19
+ questionIntake: z
20
+ .object({
21
+ enabled: z
22
+ .boolean()
23
+ .default(false)
24
+ .meta({
25
+ env: ["AGENT_QUESTION_INTAKE_EXPERIMENT_ENABLED"],
26
+ doc: "Enable signup-time assignment and run-level question-intake experimentation for this app.",
27
+ }),
28
+ disabled: z
29
+ .boolean()
30
+ .default(false)
31
+ .meta({
32
+ env: ["AGENT_QUESTION_INTAKE_EXPERIMENT_DISABLED"],
33
+ doc: "Emergency switch that disables this app's question-intake experiment, including when enabled by app config.",
34
+ }),
35
+ experimentId: z.string().trim().min(1),
36
+ questionToolNames: z.array(z.string().trim().min(1)).default([]),
37
+ treatmentInstructions: z.string().trim().min(1),
38
+ trackedEventNames: z.array(z.string().trim().min(1)).default([]),
39
+ clientTrackedEventNames: z
40
+ .array(z.string().trim().min(1))
41
+ .default([])
42
+ .meta({
43
+ doc: "Client-originated event names allowed to receive experiment assignment metadata. Keep outcome events restricted to server-confirmed behavior.",
44
+ }),
45
+ })
46
+ .optional()
47
+ .meta({
48
+ doc: "App-specific settings for the reusable question-intake experiment. Only new users created while enabled receive an assignment.",
49
+ }),
19
50
  builtInEngines: z
20
51
  .array(z.string().min(1))
21
52
  .min(1)
@@ -26,6 +26,15 @@ export declare const appConfigSchema: z.ZodObject<{
26
26
  agent: z.ZodPrefault<z.ZodObject<{
27
27
  engine: z.ZodOptional<z.ZodString>;
28
28
  model: z.ZodOptional<z.ZodString>;
29
+ questionIntake: z.ZodOptional<z.ZodObject<{
30
+ enabled: z.ZodDefault<z.ZodBoolean>;
31
+ disabled: z.ZodDefault<z.ZodBoolean>;
32
+ experimentId: z.ZodString;
33
+ questionToolNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
+ treatmentInstructions: z.ZodString;
35
+ trackedEventNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
36
+ clientTrackedEventNames: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
+ }, z.core.$strip>>;
29
38
  builtInEngines: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
39
  buildEnginePackages: z.ZodOptional<z.ZodString>;
31
40
  mode: z.ZodOptional<z.ZodString>;
@@ -26,8 +26,29 @@ function mergeLayers(base, override) {
26
26
  }
27
27
  return result;
28
28
  }
29
- function resolve(envLayer) {
30
- state.layers.env = envLayer;
29
+ function resolve(envLayer, questionIntakeEnvironment) {
30
+ const nonEnvironmentConfig = LAYER_ORDER.filter((layer) => layer !== "env").reduce((merged, layer) => state.layers[layer]
31
+ ? mergeLayers(merged, state.layers[layer])
32
+ : merged, {});
33
+ const configuredAgent = nonEnvironmentConfig.agent;
34
+ const configuredQuestionIntake = isPlainObject(configuredAgent)
35
+ ? configuredAgent.questionIntake
36
+ : undefined;
37
+ const hasQuestionIntakeConfig = isPlainObject(configuredQuestionIntake) &&
38
+ typeof configuredQuestionIntake.experimentId === "string" &&
39
+ typeof configuredQuestionIntake.treatmentInstructions === "string";
40
+ const resolvedEnvLayer = mergeLayers({}, envLayer);
41
+ if (hasQuestionIntakeConfig &&
42
+ questionIntakeEnvironment?.enabled !== undefined) {
43
+ const agentLayer = isPlainObject(resolvedEnvLayer.agent)
44
+ ? resolvedEnvLayer.agent
45
+ : (resolvedEnvLayer.agent = {});
46
+ const questionIntakeLayer = isPlainObject(agentLayer.questionIntake)
47
+ ? agentLayer.questionIntake
48
+ : (agentLayer.questionIntake = {});
49
+ questionIntakeLayer.enabled = questionIntakeEnvironment.enabled;
50
+ }
51
+ state.layers.env = resolvedEnvLayer;
31
52
  let merged = {};
32
53
  for (const layer of LAYER_ORDER) {
33
54
  const value = state.layers[layer];
@@ -35,6 +56,20 @@ function resolve(envLayer) {
35
56
  merged = mergeLayers(merged, value);
36
57
  }
37
58
  const parsed = appConfigSchema.parse(merged);
59
+ if (questionIntakeEnvironment?.enabled !== undefined &&
60
+ parsed.agent.questionIntake) {
61
+ parsed.agent.questionIntake = {
62
+ ...parsed.agent.questionIntake,
63
+ enabled: questionIntakeEnvironment.enabled,
64
+ };
65
+ }
66
+ if (questionIntakeEnvironment?.disabled === true &&
67
+ parsed.agent.questionIntake) {
68
+ parsed.agent.questionIntake = {
69
+ ...parsed.agent.questionIntake,
70
+ disabled: true,
71
+ };
72
+ }
38
73
  assertRunLifecycleInvariants(parsed.agent);
39
74
  parsed.app = deriveAppIdentity(parsed.app);
40
75
  return parsed;
@@ -68,12 +103,20 @@ export function defineAppConfig(config) {
68
103
  return () => { };
69
104
  }
70
105
  export function getAppConfig() {
71
- const envLayer = readEnvConfigLayer(appConfigSchema, readConfigEnvironment());
72
- const signature = JSON.stringify(envLayer);
106
+ const environment = readConfigEnvironment();
107
+ const questionIntakeEnvironment = readEnvConfigLayer(appConfigSchema, {
108
+ AGENT_QUESTION_INTAKE_EXPERIMENT_ENABLED: environment.AGENT_QUESTION_INTAKE_EXPERIMENT_ENABLED,
109
+ AGENT_QUESTION_INTAKE_EXPERIMENT_DISABLED: environment.AGENT_QUESTION_INTAKE_EXPERIMENT_DISABLED,
110
+ }).agent?.questionIntake;
111
+ const environmentWithoutQuestionIntakeSwitches = { ...environment };
112
+ delete environmentWithoutQuestionIntakeSwitches.AGENT_QUESTION_INTAKE_EXPERIMENT_ENABLED;
113
+ delete environmentWithoutQuestionIntakeSwitches.AGENT_QUESTION_INTAKE_EXPERIMENT_DISABLED;
114
+ const envLayer = readEnvConfigLayer(appConfigSchema, environmentWithoutQuestionIntakeSwitches);
115
+ const signature = JSON.stringify({ envLayer, questionIntakeEnvironment });
73
116
  if (state.resolved && state.envSignature === signature)
74
117
  return state.resolved;
75
118
  state.envSignature = signature;
76
- return (state.resolved = resolve(envLayer));
119
+ return (state.resolved = resolve(envLayer, questionIntakeEnvironment));
77
120
  }
78
121
  export function resetAppConfigForTests() {
79
122
  state.layers = {};
@@ -25,8 +25,8 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
25
25
  ok: boolean;
26
26
  }>>;
27
27
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
- ok?: undefined;
29
28
  text?: undefined;
29
+ ok?: undefined;
30
30
  error: string;
31
31
  } | {
32
32
  error?: undefined;
@@ -13,9 +13,9 @@ export declare const postCollabPatch: import("h3").EventHandlerWithFetch<import(
13
13
  ok: boolean;
14
14
  }>>;
15
15
  export declare const getCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- data?: undefined;
17
16
  error: string;
18
17
  docId?: undefined;
18
+ data?: undefined;
19
19
  } | {
20
20
  error?: undefined;
21
21
  docId: string;
@@ -33,10 +33,10 @@ declare const _default: import("../../action.js").ActionDefinition<{
33
33
  idempotencyKey: string;
34
34
  deleted: boolean;
35
35
  } | {
36
- id?: undefined;
36
+ provider?: undefined;
37
37
  error: string;
38
38
  url?: undefined;
39
- provider?: undefined;
39
+ id?: undefined;
40
40
  configured?: undefined;
41
41
  } | {
42
42
  error?: undefined;
@@ -45,9 +45,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
45
45
  provider: string;
46
46
  configured?: undefined;
47
47
  } | {
48
- id?: undefined;
49
- url?: undefined;
50
48
  provider?: undefined;
49
+ url?: undefined;
50
+ id?: undefined;
51
51
  error: string;
52
52
  configured: boolean;
53
53
  }>;
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import { currentExecutionFence, StaleExecutionFenceError, withFencedExecutionWrite, } from "../agent/execution-fence.js";
4
4
  import { resolveBackgroundRunHardTimeoutMs } from "../agent/run-manager.js";
5
- import { getDbExec } from "../db/client.js";
5
+ import { getDbExec, getScopedDbExec, } from "../db/client.js";
6
6
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
7
7
  import { defineStore } from "../db/store-registry.js";
8
8
  import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
@@ -717,7 +717,8 @@ export async function finishAutomationRun(id, status, error, errorCode, options
717
717
  }
718
718
  }
719
719
  export async function attachAutomationRunThread(id, threadId, runId, options = {}) {
720
- await ensureTable();
720
+ if (getScopedDbExec() === undefined)
721
+ await ensureTable();
721
722
  const fence = currentExecutionFence();
722
723
  if (fence && (fence.runId !== runId || fence.threadId !== threadId)) {
723
724
  throw new StaleExecutionFenceError(fence);
@@ -60,4 +60,4 @@ export declare function hostedModelExperimentVariant(assignments: readonly Hoste
60
60
  * The singular experiment tags predate the hosted test. They keep naming only
61
61
  * the other experiment on a turn, so the hosted arm never changes them.
62
62
  */
63
- export declare function singleNonHostedAssignment(assignments: readonly HostedModelExperimentAssignment[] | undefined): HostedModelExperimentAssignment | undefined;
63
+ export declare function singleNonHostedAssignment(assignments: readonly HostedModelExperimentAssignment[] | undefined, selectedAssignment?: HostedModelExperimentAssignment): HostedModelExperimentAssignment | undefined;
@@ -86,7 +86,13 @@ export function hostedModelExperimentVariant(assignments) {
86
86
  * The singular experiment tags predate the hosted test. They keep naming only
87
87
  * the other experiment on a turn, so the hosted arm never changes them.
88
88
  */
89
- export function singleNonHostedAssignment(assignments) {
89
+ export function singleNonHostedAssignment(assignments, selectedAssignment) {
90
+ if (selectedAssignment &&
91
+ selectedAssignment.experimentId !== HOSTED_MODEL_EXPERIMENT_ID &&
92
+ assignments?.some((assignment) => assignment.experimentId === selectedAssignment.experimentId &&
93
+ assignment.variantId === selectedAssignment.variantId)) {
94
+ return selectedAssignment;
95
+ }
90
96
  const others = assignments?.filter((assignment) => assignment.experimentId !== HOSTED_MODEL_EXPERIMENT_ID);
91
97
  return others?.length === 1 ? others[0] : undefined;
92
98
  }
@@ -0,0 +1,34 @@
1
+ export declare const QUESTION_INTAKE_CONTROL_VARIANT: "control";
2
+ export declare const QUESTION_INTAKE_TREATMENT_VARIANT: "no_questions";
3
+ export interface QuestionIntakeExperimentConfig {
4
+ enabled: boolean;
5
+ disabled?: boolean;
6
+ experimentId: string;
7
+ questionToolNames: string[];
8
+ treatmentInstructions: string;
9
+ trackedEventNames: string[];
10
+ clientTrackedEventNames?: string[];
11
+ }
12
+ export interface QuestionIntakeExperimentAssignment {
13
+ authUserId: string;
14
+ experimentId: string;
15
+ variantId: typeof QUESTION_INTAKE_CONTROL_VARIANT | typeof QUESTION_INTAKE_TREATMENT_VARIANT;
16
+ }
17
+ export declare function questionIntakeExposureProperties(assignment: QuestionIntakeExperimentAssignment, context: {
18
+ appId: string;
19
+ threadId?: string;
20
+ turnId: string;
21
+ }): Record<string, string>;
22
+ export declare class QuestionIntakeAssignmentError extends Error {
23
+ readonly code = "question_intake_assignment_failed";
24
+ constructor(experimentId: string, cause: unknown);
25
+ }
26
+ export declare class InvalidQuestionIntakeExperimentAssignmentError extends Error {
27
+ readonly code = "question_intake_assignment_invalid";
28
+ constructor(experimentId: string);
29
+ }
30
+ export declare function treatmentInstructionsForQuestionIntake(config: QuestionIntakeExperimentConfig | undefined, assignment: QuestionIntakeExperimentAssignment | null): string | undefined;
31
+ /** Assigns only from the user-create hook while the app config is enabled. */
32
+ export declare function assignQuestionIntakeExperimentOnSignup(config: QuestionIntakeExperimentConfig | undefined, authUserId: string | undefined): Promise<QuestionIntakeExperimentAssignment | null>;
33
+ /** Reads an existing signup assignment; agent runs never enroll older users. */
34
+ export declare function resolveQuestionIntakeExperimentAssignment(config: QuestionIntakeExperimentConfig | undefined, authUserId: string | undefined): Promise<QuestionIntakeExperimentAssignment | null>;
@@ -0,0 +1,114 @@
1
+ import { createHash } from "node:crypto";
2
+ import { getAssignment, upsertAssignment } from "./store.js";
3
+ export const QUESTION_INTAKE_CONTROL_VARIANT = "control";
4
+ export const QUESTION_INTAKE_TREATMENT_VARIANT = "no_questions";
5
+ export function questionIntakeExposureProperties(assignment, context) {
6
+ return {
7
+ experiment_id: assignment.experimentId,
8
+ experiment_variant: assignment.variantId,
9
+ exposure_surface: "agent_run",
10
+ app_id: context.appId,
11
+ templateId: context.appId,
12
+ agent_native_app: context.appId,
13
+ ...(context.threadId ? { thread_id: context.threadId } : {}),
14
+ turn_id: context.turnId,
15
+ };
16
+ }
17
+ export class QuestionIntakeAssignmentError extends Error {
18
+ code = "question_intake_assignment_failed";
19
+ constructor(experimentId, cause) {
20
+ super(`Question-intake assignment failed after retry for ${experimentId}.`, { cause });
21
+ this.name = "QuestionIntakeAssignmentError";
22
+ }
23
+ }
24
+ export class InvalidQuestionIntakeExperimentAssignmentError extends Error {
25
+ code = "question_intake_assignment_invalid";
26
+ constructor(experimentId) {
27
+ super(`Question-intake assignment has an invalid stored variant for ${experimentId}.`);
28
+ this.name = "InvalidQuestionIntakeExperimentAssignmentError";
29
+ }
30
+ }
31
+ export function treatmentInstructionsForQuestionIntake(config, assignment) {
32
+ if (!configured(config) ||
33
+ assignment?.experimentId !== config.experimentId ||
34
+ assignment.variantId !== QUESTION_INTAKE_TREATMENT_VARIANT) {
35
+ return undefined;
36
+ }
37
+ return [
38
+ "For this assigned experiment treatment, the instructions below override conflicting intake-question directives in the app prompt or the user's current message, including app-injected directives.",
39
+ config.treatmentInstructions.trim(),
40
+ ].join("\n");
41
+ }
42
+ function configured(config) {
43
+ return Boolean(config?.enabled &&
44
+ !config.disabled &&
45
+ config.experimentId.trim() &&
46
+ config.questionToolNames.length > 0 &&
47
+ config.treatmentInstructions.trim());
48
+ }
49
+ function variantForUser(experimentId, authUserId) {
50
+ const bucket = createHash("sha256")
51
+ .update(`${experimentId}\0${authUserId}`)
52
+ .digest()
53
+ .readUInt8(0);
54
+ return bucket < 128
55
+ ? QUESTION_INTAKE_CONTROL_VARIANT
56
+ : QUESTION_INTAKE_TREATMENT_VARIANT;
57
+ }
58
+ function assignmentFromStored(config, authUserId, variantId) {
59
+ if (variantId !== QUESTION_INTAKE_CONTROL_VARIANT &&
60
+ variantId !== QUESTION_INTAKE_TREATMENT_VARIANT) {
61
+ throw new InvalidQuestionIntakeExperimentAssignmentError(config.experimentId);
62
+ }
63
+ return { authUserId, experimentId: config.experimentId, variantId };
64
+ }
65
+ /** Assigns only from the user-create hook while the app config is enabled. */
66
+ // raw-sql-ok: authUserId is persisted only through the assignment store's bound arguments.
67
+ export async function assignQuestionIntakeExperimentOnSignup(config, authUserId) {
68
+ if (!configured(config) || !authUserId?.trim())
69
+ return null;
70
+ const assign = async () => {
71
+ // raw-sql-ok: the assignment store binds the experiment and user IDs as query arguments.
72
+ const existing = await getAssignment(config.experimentId, authUserId);
73
+ if (existing) {
74
+ return assignmentFromStored(config, authUserId, existing.variantId);
75
+ }
76
+ const variantId = variantForUser(config.experimentId, authUserId);
77
+ await upsertAssignment({
78
+ experimentId: config.experimentId,
79
+ userId: authUserId,
80
+ variantId,
81
+ assignedAt: Date.now(),
82
+ });
83
+ return { authUserId, experimentId: config.experimentId, variantId };
84
+ };
85
+ try {
86
+ return await assign();
87
+ }
88
+ catch (firstError) {
89
+ if (firstError instanceof InvalidQuestionIntakeExperimentAssignmentError) {
90
+ throw firstError;
91
+ }
92
+ await new Promise((resolve) => setTimeout(resolve, 50));
93
+ try {
94
+ return await assign();
95
+ }
96
+ catch (retryError) {
97
+ if (retryError instanceof InvalidQuestionIntakeExperimentAssignmentError) {
98
+ throw retryError;
99
+ }
100
+ throw new QuestionIntakeAssignmentError(config.experimentId, new AggregateError([firstError, retryError], "Question-intake assignment failed twice."));
101
+ }
102
+ }
103
+ }
104
+ /** Reads an existing signup assignment; agent runs never enroll older users. */
105
+ // raw-sql-ok: authUserId is read only through the assignment store's bound arguments.
106
+ export async function resolveQuestionIntakeExperimentAssignment(config, authUserId) {
107
+ if (!configured(config) || !authUserId?.trim())
108
+ return null;
109
+ // raw-sql-ok: the assignment store binds the experiment and user IDs as query arguments.
110
+ const existing = await getAssignment(config.experimentId, authUserId);
111
+ if (!existing)
112
+ return null;
113
+ return assignmentFromStored(config, authUserId, existing.variantId);
114
+ }
@@ -49,6 +49,10 @@ export declare function instrumentAgentLoop(opts: {
49
49
  experimentId: string;
50
50
  variantId: string;
51
51
  }>;
52
+ singularExperimentAssignment?: {
53
+ experimentId: string;
54
+ variantId: string;
55
+ };
52
56
  modelSelectionSource?: string;
53
57
  delegation?: {
54
58
  protocol: "a2a" | "mcp" | "agent-team";
@@ -311,7 +311,7 @@ function emitLlmGenerationTrackingEvent(args) {
311
311
  properties.experiment_variants = args.experimentAssignments
312
312
  .map((assignment) => assignment.variantId)
313
313
  .join(",");
314
- const singular = singleNonHostedAssignment(args.experimentAssignments);
314
+ const singular = singleNonHostedAssignment(args.experimentAssignments, args.singularExperimentAssignment);
315
315
  if (singular) {
316
316
  properties.experiment_id = singular.experimentId;
317
317
  properties.experiment_variant = singular.variantId;
@@ -404,6 +404,7 @@ export async function instrumentAgentLoop(opts) {
404
404
  .catch(() => null)
405
405
  : Promise.resolve(null);
406
406
  const browserSessionId = opts.browserSessionId ?? getRequestContext()?.browserSessionId;
407
+ const singularExperimentAssignment = singleNonHostedAssignment(opts.experimentAssignments, opts.singularExperimentAssignment);
407
408
  const otelRunSpanPromise = startAgentSpan("invoke_agent", {
408
409
  "gen_ai.operation.name": "invoke_agent",
409
410
  "gen_ai.provider.name": engineName,
@@ -412,9 +413,8 @@ export async function instrumentAgentLoop(opts) {
412
413
  "agent.run_id": runId,
413
414
  "agent.user_id": userId ?? undefined,
414
415
  "agent.model_selection_source": opts.modelSelectionSource,
415
- "agent.experiment_id": singleNonHostedAssignment(opts.experimentAssignments)
416
- ?.experimentId,
417
- "agent.experiment_variant": singleNonHostedAssignment(opts.experimentAssignments)?.variantId,
416
+ "agent.experiment_id": singularExperimentAssignment?.experimentId,
417
+ "agent.experiment_variant": singularExperimentAssignment?.variantId,
418
418
  "agent.hosted_model_experiment_variant": hostedModelExperimentVariant(opts.experimentAssignments),
419
419
  });
420
420
  let otelRunSpan = null;
@@ -1265,6 +1265,7 @@ export async function instrumentAgentLoop(opts) {
1265
1265
  createdAt: generation.createdAt,
1266
1266
  endedAt: generation.endedAt,
1267
1267
  experimentAssignments: opts.experimentAssignments,
1268
+ singularExperimentAssignment: opts.singularExperimentAssignment,
1268
1269
  modelSelectionSource: opts.modelSelectionSource,
1269
1270
  browserSessionId,
1270
1271
  ...generationContent,