@agent-native/core 0.228.0-nightly-20261111212500 → 0.228.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent/engine/builder-engine.js +5 -4
  2. package/dist/agent/engine/error-detail.d.ts +1 -0
  3. package/dist/agent/engine/error-detail.js +5 -0
  4. package/dist/agent/production-agent.js +2 -2
  5. package/dist/app-config/onboarding.d.ts +1 -0
  6. package/dist/app-config/onboarding.js +1 -0
  7. package/dist/app-config/schema.d.ts +1 -0
  8. package/dist/cli/index.d.ts +7 -1
  9. package/dist/cli/index.js +19 -3
  10. package/dist/client/chat/agentkit-agent-native.js +145 -0
  11. package/dist/client/chat/agentkit-protocol.js +8 -2
  12. package/dist/client/error-format.js +5 -1
  13. package/dist/coding-tools/run-code-result.d.ts +11 -0
  14. package/dist/coding-tools/run-code-result.js +39 -0
  15. package/dist/coding-tools/run-code.js +6 -6
  16. package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
  17. package/dist/collab/routes.d.ts +1 -1
  18. package/dist/decisions/actions/decide.d.ts +1 -0
  19. package/dist/decisions/actions/decide.js +1 -0
  20. package/dist/decisions/decide.js +2 -1
  21. package/dist/decisions/types.d.ts +1 -0
  22. package/dist/extensions/routes.js +1 -1
  23. package/dist/framework-package-versions.json +11 -11
  24. package/dist/integrations/slack-input-request.d.ts +2 -0
  25. package/dist/integrations/slack-input-request.js +87 -0
  26. package/dist/integrations/webhook-handler.js +10 -58
  27. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  28. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/scripts/db/safety.js +1 -0
  31. package/dist/server/agent-chat/context-tools.js +59 -23
  32. package/dist/server/builder-signup-experiment.js +4 -1
  33. package/dist/shared/ssr-session-bootstrap.js +5 -1
  34. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +10 -3
  35. package/dist/templates/chat/app/global.css +0 -5
  36. package/dist/vite/client.js +17 -0
  37. package/docs/content/dispatch.mdx +1 -1
  38. package/docs/content/multi-app-workspace.mdx +1 -1
  39. package/docs/content/security.mdx +1 -1
  40. package/docs/content/template-dispatch-reference.mdx +1 -1
  41. package/docs/content/workspace-management.mdx +0 -1
  42. package/package.json +4 -4
  43. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +10 -3
  44. package/src/templates/chat/app/global.css +0 -5
@@ -0,0 +1,87 @@
1
+ /** One `ask-question` call's input as a Slack message, or null if Slack cannot show it. */
2
+ export function slackInputRequestText(input) {
3
+ let items = [input];
4
+ // Absent, null or empty `questions` means the single-question form, as in the tool.
5
+ if (input?.questions !== undefined &&
6
+ input.questions !== null &&
7
+ input.questions !== "") {
8
+ try {
9
+ const parsed = Array.isArray(input.questions)
10
+ ? input.questions
11
+ : JSON.parse(stringifyInboundValue(input.questions));
12
+ if (!Array.isArray(parsed))
13
+ return null;
14
+ items = parsed.slice(0, 4);
15
+ // coercion-ok: questions Slack cannot read mean no input request; the run's own reply is sent instead.
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ const parsedBlocks = items.map((item) => slackQuestionBlock(item));
22
+ // A question Slack cannot show would leave the others unanswerable.
23
+ if (!parsedBlocks.length || parsedBlocks.includes(null))
24
+ return null;
25
+ const blocks = parsedBlocks.filter((block) => block !== null);
26
+ const allowFreeText = blocks.some((block) => block.allowFreeText);
27
+ const severalChoices = blocks.length > 1 || blocks.some((block) => block.allowMultiple);
28
+ return [
29
+ ...blocks.flatMap((block, blockIndex) => [
30
+ ...(blockIndex > 0 ? [""] : []),
31
+ ...block.lines,
32
+ ]),
33
+ "",
34
+ `Reply in this thread with your choice${severalChoices ? "s" : ""}${allowFreeText ? " or a short answer" : ""}.`,
35
+ ].join("\n");
36
+ }
37
+ function stringifyInboundValue(value) {
38
+ return typeof value === "string" ? value : JSON.stringify(value);
39
+ }
40
+ function slackQuestionBlock(input) {
41
+ const question = typeof input?.question === "string" ? input.question.trim() : "";
42
+ if (!question)
43
+ return null;
44
+ let rawOptions;
45
+ try {
46
+ rawOptions = Array.isArray(input?.options)
47
+ ? input.options
48
+ : JSON.parse(stringifyInboundValue(input?.options ?? "[]"));
49
+ // coercion-ok: options Slack cannot read mean no input request; the run's own reply is sent instead.
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0)
55
+ return null;
56
+ const options = rawOptions
57
+ .slice(0, 4)
58
+ .map((option) => {
59
+ const value = option;
60
+ const label = typeof value?.label === "string"
61
+ ? value.label.trim()
62
+ : typeof value?.value === "string"
63
+ ? value.value.trim()
64
+ : "";
65
+ if (!label)
66
+ return null;
67
+ const description = typeof value?.description === "string" ? value.description.trim() : "";
68
+ return {
69
+ label: label.slice(0, 200),
70
+ description: description.slice(0, 400),
71
+ };
72
+ })
73
+ .filter((option) => option !== null);
74
+ if (!options.length)
75
+ return null;
76
+ const header = typeof input?.header === "string" ? input.header.trim().slice(0, 80) : "";
77
+ return {
78
+ allowFreeText: stringifyInboundValue(input?.allowFreeText ?? "true") !== "false",
79
+ allowMultiple: stringifyInboundValue(input?.allowMultiple ?? "false") === "true",
80
+ lines: [
81
+ ...(header ? [`*${header}*`] : []),
82
+ question.slice(0, 1_500),
83
+ "",
84
+ ...options.map((option, optionIndex) => `${optionIndex + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`),
85
+ ],
86
+ };
87
+ }
@@ -31,6 +31,7 @@ import { loadIntegrationMemoryPrompt } from "./integration-memory.js";
31
31
  import { insertPendingTask, isDuplicateEventError, stageTaskDeliveryPayload, } from "./pending-tasks-store.js";
32
32
  import { integrationRequestContext, resolveIntegrationRequestScope, } from "./request-scope.js";
33
33
  import { integrationScopeSubjectKey } from "./scope-store.js";
34
+ import { slackInputRequestText } from "./slack-input-request.js";
34
35
  import { getThreadMapping, saveThreadMapping } from "./thread-mapping-store.js";
35
36
  import { listIntegrationUsageBudgets, releaseIntegrationUsageBudget, reserveIntegrationUsageBudget, settleIntegrationUsageBudget, } from "./usage-budget-store.js";
36
37
  const PROCESSOR_DISPATCH_SETTLE_WAIT_MS = 1_500;
@@ -1457,64 +1458,15 @@ function extractSlackInputRequest(completedRun) {
1457
1458
  if (!delivered)
1458
1459
  return null;
1459
1460
  const deliveredId = delivered.type === "tool_done" ? delivered.id : "";
1460
- for (let index = events.length - 1; index >= 0; index--) {
1461
- const event = events[index];
1462
- if (event.type !== "tool_start" ||
1463
- event.tool !== "ask-question" ||
1464
- event.id !== deliveredId) {
1465
- continue;
1466
- }
1467
- const input = event.input;
1468
- const question = typeof input?.question === "string" ? input.question.trim() : "";
1469
- if (!question)
1470
- return null;
1471
- let rawOptions;
1472
- try {
1473
- rawOptions = JSON.parse(stringifyInboundValue(input?.options ?? "[]"));
1474
- }
1475
- catch {
1476
- return null;
1477
- }
1478
- if (!Array.isArray(rawOptions) || rawOptions.length === 0)
1479
- return null;
1480
- const options = rawOptions
1481
- .slice(0, 4)
1482
- .map((option) => {
1483
- const value = option;
1484
- const label = typeof value?.label === "string"
1485
- ? value.label.trim()
1486
- : typeof value?.value === "string"
1487
- ? value.value.trim()
1488
- : "";
1489
- if (!label)
1490
- return null;
1491
- const description = typeof value?.description === "string"
1492
- ? value.description.trim()
1493
- : "";
1494
- return {
1495
- label: label.slice(0, 200),
1496
- description: description.slice(0, 400),
1497
- };
1498
- })
1499
- .filter((option) => option !== null);
1500
- if (!options.length)
1501
- return null;
1502
- const header = typeof input?.header === "string" ? input.header.trim().slice(0, 80) : "";
1503
- const allowFreeText = stringifyInboundValue(input?.allowFreeText ?? "true") !== "false";
1504
- return {
1505
- text: [
1506
- header ? `*${header}*` : null,
1507
- question.slice(0, 1_500),
1508
- "",
1509
- ...options.map((option, optionIndex) => `${optionIndex + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`),
1510
- "",
1511
- `Reply in this thread with your choice${allowFreeText ? " or a short answer" : ""}.`,
1512
- ]
1513
- .filter((line) => line !== null)
1514
- .join("\n"),
1515
- };
1516
- }
1517
- return null;
1461
+ const started = [...events]
1462
+ .reverse()
1463
+ .find((event) => event.type === "tool_start" &&
1464
+ event.tool === "ask-question" &&
1465
+ event.id === deliveredId);
1466
+ if (started?.type !== "tool_start")
1467
+ return null;
1468
+ const text = slackInputRequestText(started.input);
1469
+ return text === null ? null : { text };
1518
1470
  }
1519
1471
  function extractRecoverableA2AArtifactToolResult(completedRun) {
1520
1472
  for (let i = completedRun.events.length - 1; i >= 0; i--) {
@@ -75,9 +75,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- deleted?: undefined;
79
78
  id?: undefined;
80
- provider?: undefined;
81
79
  message?: undefined;
82
80
  found?: undefined;
83
81
  providers: {
@@ -90,44 +88,46 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
90
88
  updatedAt: number;
91
89
  }[];
92
90
  count: number;
91
+ provider?: undefined;
92
+ deleted?: undefined;
93
93
  registered?: undefined;
94
94
  label?: undefined;
95
95
  } | {
96
- deleted?: undefined;
97
96
  id?: undefined;
97
+ count?: undefined;
98
98
  message?: undefined;
99
99
  providers?: undefined;
100
- count?: undefined;
101
100
  found: boolean;
102
101
  provider: import("../custom-registry.js").CustomProviderConfig;
102
+ deleted?: undefined;
103
103
  registered?: undefined;
104
104
  label?: undefined;
105
105
  } | {
106
- deleted?: undefined;
107
- provider?: undefined;
106
+ count?: undefined;
108
107
  message?: undefined;
109
108
  providers?: undefined;
110
- count?: undefined;
109
+ provider?: undefined;
111
110
  found: boolean;
112
111
  id: string;
112
+ deleted?: undefined;
113
113
  registered?: undefined;
114
114
  label?: undefined;
115
115
  } | {
116
- provider?: undefined;
116
+ count?: undefined;
117
117
  message?: undefined;
118
118
  found?: undefined;
119
119
  providers?: undefined;
120
- count?: undefined;
120
+ provider?: undefined;
121
121
  deleted: boolean;
122
122
  id: string;
123
123
  registered?: undefined;
124
124
  label?: undefined;
125
125
  } | {
126
- deleted?: undefined;
127
- provider?: undefined;
126
+ count?: undefined;
128
127
  found?: undefined;
129
128
  providers?: undefined;
130
- count?: undefined;
129
+ provider?: undefined;
130
+ deleted?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;
@@ -316,7 +316,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
316
316
  notes?: string | undefined;
317
317
  scope?: "org" | "user" | undefined;
318
318
  }, {
319
+ deleted?: undefined;
319
320
  id?: undefined;
321
+ provider?: undefined;
320
322
  found?: undefined;
321
323
  message?: undefined;
322
324
  providers: {
@@ -329,46 +331,44 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
329
331
  updatedAt: number;
330
332
  }[];
331
333
  count: number;
332
- provider?: undefined;
333
- deleted?: undefined;
334
334
  registered?: undefined;
335
335
  label?: undefined;
336
336
  } | {
337
+ deleted?: undefined;
337
338
  id?: undefined;
338
- count?: undefined;
339
339
  message?: undefined;
340
340
  providers?: undefined;
341
+ count?: undefined;
341
342
  found: boolean;
342
343
  provider: import("../custom-registry.js").CustomProviderConfig;
343
- deleted?: undefined;
344
344
  registered?: undefined;
345
345
  label?: undefined;
346
346
  } | {
347
- count?: undefined;
347
+ deleted?: undefined;
348
+ provider?: undefined;
348
349
  message?: undefined;
349
350
  providers?: undefined;
350
- provider?: undefined;
351
+ count?: undefined;
351
352
  found: boolean;
352
353
  id: string;
353
- deleted?: undefined;
354
354
  registered?: undefined;
355
355
  label?: undefined;
356
356
  } | {
357
- count?: undefined;
357
+ provider?: undefined;
358
358
  found?: undefined;
359
359
  message?: undefined;
360
360
  providers?: undefined;
361
- provider?: undefined;
361
+ count?: undefined;
362
362
  deleted: boolean;
363
363
  id: string;
364
364
  registered?: undefined;
365
365
  label?: undefined;
366
366
  } | {
367
- count?: undefined;
367
+ deleted?: undefined;
368
+ provider?: undefined;
368
369
  found?: undefined;
369
370
  providers?: undefined;
370
- provider?: undefined;
371
- deleted?: undefined;
371
+ count?: undefined;
372
372
  registered: boolean;
373
373
  id: string;
374
374
  label: string;
@@ -49,6 +49,7 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
49
49
  error: string;
50
50
  storageSetupRequired?: undefined;
51
51
  } | {
52
+ error?: undefined;
52
53
  id: string;
53
54
  path: string;
54
55
  owner: string;
@@ -63,7 +64,6 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
63
64
  runId: string | null;
64
65
  expiresAt: number | null;
65
66
  metadata: string | null;
66
- error?: undefined;
67
67
  url: string;
68
68
  provider: string;
69
69
  storageSetupRequired?: undefined;
@@ -40,6 +40,7 @@ function isPunctuation(token, text) {
40
40
  }
41
41
  const SENSITIVE_FRAMEWORK_TABLES = new Set([
42
42
  "app_secrets",
43
+ "vault_secrets",
43
44
  "oauth_tokens",
44
45
  "mcp_oauth_clients",
45
46
  "mcp_oauth_codes",
@@ -469,62 +469,98 @@ function parseAskQuestion(args) {
469
469
  }
470
470
  return { question, header, allowMultiple, allowFreeText, options };
471
471
  }
472
+ const MAX_ASK_QUESTIONS = 4;
473
+ function parseAskQuestions(args) {
474
+ const raw = typeof args === "object" && args !== null
475
+ ? args.questions
476
+ : undefined;
477
+ if (raw === undefined || raw === null || raw === "") {
478
+ return { questions: [parseAskQuestion(args)] };
479
+ }
480
+ let parsed = raw;
481
+ if (!Array.isArray(raw)) {
482
+ try {
483
+ parsed = JSON.parse(String(raw));
484
+ }
485
+ catch {
486
+ throw new ActionInputValidationError("'questions' must be a JSON array of { question, header?, options, allowMultiple?, allowFreeText? }.");
487
+ }
488
+ }
489
+ if (!Array.isArray(parsed) ||
490
+ parsed.length === 0 ||
491
+ parsed.length > MAX_ASK_QUESTIONS) {
492
+ throw new ActionInputValidationError(`'questions' must be a JSON array of 1 to ${MAX_ASK_QUESTIONS} questions.`);
493
+ }
494
+ return {
495
+ questions: parsed.map((item) => {
496
+ const fields = (item ?? {});
497
+ return parseAskQuestion({
498
+ ...fields,
499
+ options: Array.isArray(fields.options)
500
+ ? JSON.stringify(fields.options)
501
+ : fields.options,
502
+ });
503
+ }),
504
+ };
505
+ }
472
506
  function createAskQuestionTool() {
473
507
  const entry = {
474
508
  endsTurn: true,
475
509
  tool: {
476
- description: "Ask the user a multiple-choice clarifying question and render it inline in the chat. Use this ONLY when you are genuinely blocked on a decision you cannot resolve from context and a wrong guess would be costly — an ambiguous metric, date range, or grain; a real fork in approach. Present 2-5 concrete options and mark the most likely one recommended. Do NOT use it for confirmations, for things the user already specified, or to dodge easy work you could just do. Calling this ends the turn: one question per turn, and any other tool call you emit alongside it will not run.",
510
+ description: "Ask 1-4 multiple-choice clarifying questions in a card above the message box. Use ONLY when blocked on a costly decision you cannot resolve from context (an ambiguous metric, date range or grain; a real fork in approach), never for confirmations, things already specified, or easy work. One question: `question` and `options`; several: `questions`, answered together in one message. Mark the likely option recommended. Ends the turn: other tool calls in the same response will not run.",
477
511
  parameters: {
478
512
  type: "object",
479
513
  properties: {
480
514
  question: {
481
515
  type: "string",
482
- description: "The complete question to ask the user. Clear, specific, ends with a question mark.",
516
+ description: "The question, with the context needed to choose, ending in a question mark. Omit it with `questions`.",
517
+ },
518
+ questions: {
519
+ type: "string",
520
+ description: 'Instead of `question` and `options`: a JSON array of 1-4 `{ "question", "header"?, "options", "allowMultiple"?, "allowFreeText"? }`, each field as below.',
483
521
  },
484
522
  header: {
485
523
  type: "string",
486
- description: 'Optional very short label (max ~12 chars) shown as a chip/heading above the question, e.g. "Date range", "Approach", "Library".',
524
+ description: 'Optional short chip label (about 12 characters), e.g. "Date range".',
487
525
  },
488
526
  options: {
489
527
  type: "string",
490
- description: 'A JSON array of 2-4 distinct, mutually-exclusive options (unless `allowMultiple` is true), each `{ "label": string, "value"?: string, "description"?: string, "preview"?: string, "recommended"?: boolean }`. `label` is 1-5 words; `description` explains the trade-off; `preview` is optional content (mockup, code snippet, short comparison) rendered under the option. `value` defaults to `label` when omitted. Mark the most likely option `"recommended": true`. Do NOT add an "Other" option — free text is provided automatically when `allowFreeText` is on.',
528
+ description: 'A JSON array of 2-4 distinct options (exclusive unless `allowMultiple`), each `{ "label", "value"?, "recommended"? }`. The user sees only the label, on one line, so it says what you would do, specific to this case, in 3-10 words ("Checker fix only", not "Option A"); context every option shares goes in the question. `value` defaults to `label`. Add no "Other" option: `allowFreeText` provides one.',
491
529
  },
492
530
  allowFreeText: {
493
531
  type: "string",
494
- description: 'Whether the user may also type a free-text "Other" answer. Keep this "true" (the default) for preferences and clarifying questions. Use "false" only when the underlying workflow can accept one of the enumerated values and cannot handle a custom answer.',
532
+ description: 'Allow a typed "Other" answer. Keep "true" (the default) unless the workflow accepts only the listed values.',
495
533
  enum: ["true", "false"],
496
534
  },
497
535
  allowMultiple: {
498
536
  type: "string",
499
- description: 'Whether the user may select more than one option (multi-select). "true" or "false" (default).',
537
+ description: 'Allow more than one option. "true" or "false" (the default).',
500
538
  enum: ["true", "false"],
501
539
  },
502
540
  },
503
- required: ["question", "options"],
541
+ required: [],
504
542
  },
505
543
  },
506
544
  run: async (args) => {
507
- const { question, header, allowMultiple, allowFreeText, options } = args;
545
+ const { questions } = args;
508
546
  const askingRunCtx = getRequestRunContext();
509
547
  const askingThreadId = askingRunCtx?.threadId && askingRunCtx.threadId !== askingRunCtx.runId
510
548
  ? askingRunCtx.threadId
511
549
  : undefined;
512
550
  const payload = {
513
551
  ...(askingThreadId ? { threadId: askingThreadId } : {}),
514
- questions: [
515
- {
516
- id: "q1",
517
- type: "text-options",
518
- question,
519
- ...(header ? { header } : {}),
520
- required: !allowFreeText,
521
- multiSelect: allowMultiple,
522
- allowOther: allowFreeText,
523
- includeExplore: false,
524
- includeDecide: false,
525
- options,
526
- },
527
- ],
552
+ questions: questions.map(({ question, header, allowMultiple, allowFreeText, options }, index) => ({
553
+ id: `q${index + 1}`,
554
+ type: "text-options",
555
+ question,
556
+ ...(header ? { header } : {}),
557
+ required: !allowFreeText,
558
+ multiSelect: allowMultiple,
559
+ allowOther: allowFreeText,
560
+ includeExplore: false,
561
+ includeDecide: false,
562
+ options,
563
+ })),
528
564
  };
529
565
  const { writeAppState } = await import("../../application-state/script-helpers.js");
530
566
  await writeAppState(appStateKeyForBrowserTab("guided-questions", getRequestRunContext()?.browserTabId), payload);
@@ -533,7 +569,7 @@ function createAskQuestionTool() {
533
569
  };
534
570
  const action = defineAction({
535
571
  description: entry.tool.description,
536
- schema: z.unknown().transform(parseAskQuestion),
572
+ schema: z.unknown().transform(parseAskQuestions),
537
573
  endsTurn: true,
538
574
  run: entry.run,
539
575
  });
@@ -330,9 +330,12 @@ export function createBuilderSignupExperiment(dependencies) {
330
330
  if (session)
331
331
  return { status: "disabled" };
332
332
  if (!assignment) {
333
+ const { builderSignupAutomaticPercent } = getAppConfig().onboarding;
333
334
  assignment = {
334
335
  id: randomBytes(16).toString("hex"),
335
- variant: randomInt(2) === 0 ? "control" : "automatic",
336
+ variant: randomInt(100) < builderSignupAutomaticPercent
337
+ ? "automatic"
338
+ : "control",
336
339
  issuedAt: Date.now(),
337
340
  policy: BUILDER_SIGNUP_POLICY,
338
341
  };
@@ -68,8 +68,12 @@ export function isSessionNavigationPending() {
68
68
  typeof window[SESSION_NAVIGATION_FLAG] === "string");
69
69
  }
70
70
  function releaseSessionNavigation(href) {
71
- if (window[SESSION_NAVIGATION_FLAG] !== href)
71
+ // The stall timer can outlive window: a test environment tears it down
72
+ // while the timer is still pending.
73
+ if (typeof window === "undefined" ||
74
+ window[SESSION_NAVIGATION_FLAG] !== href) {
72
75
  return;
76
+ }
73
77
  delete window[SESSION_NAVIGATION_FLAG];
74
78
  window.dispatchEvent?.(new Event(SESSION_NAVIGATION_RELEASED_EVENT));
75
79
  }
@@ -39,8 +39,9 @@ import {
39
39
  } from "@agent-native/toolkit/app/chat/agentkit-chat/connections";
40
40
  import { CoreAgentKitRoot } from "@agent-native/toolkit/app/chat/agentkit-chat/index";
41
41
  import {
42
- GuidedQuestionFlow,
42
+ GuidedQuestionCard,
43
43
  useGuidedQuestionFlow,
44
+ useRefetchGuidedQuestionsWhenAsked,
44
45
  } from "@agent-native/toolkit/app/chat/agentkit-chat/questions";
45
46
  import { isClientDraftThread } from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
46
47
  import {
@@ -899,6 +900,8 @@ function ChatInitialMessage({
899
900
  control.sendMessage({
900
901
  text: requestText,
901
902
  attachments: [...(composerOptions?.uploadedAttachments ?? [])],
903
+ // The run reads mentions from here; metadata only records them.
904
+ ...(references.length ? { references: [...references] } : {}),
902
905
  options: runOptions,
903
906
  metadata,
904
907
  queueWhileRunning: metadata.custom.agentNativeQueueWhileRunning,
@@ -1048,6 +1051,7 @@ function ChatAgentFooter({ children }: { children: ReactNode }) {
1048
1051
  retryProviderStatus,
1049
1052
  handleSubmit,
1050
1053
  handleSkip,
1054
+ refetchPendingQuestion,
1051
1055
  } = useGuidedQuestionFlow({
1052
1056
  stateKey: "guided-questions",
1053
1057
  queryKey: ["guided-questions", "agentkit"],
@@ -1058,12 +1062,16 @@ function ChatAgentFooter({ children }: { children: ReactNode }) {
1058
1062
  onSubmitMessage: submitAnswers,
1059
1063
  onSkipMessage: skipQuestions,
1060
1064
  });
1065
+ useRefetchGuidedQuestionsWhenAsked({
1066
+ tools: thread.tools,
1067
+ refetch: refetchPendingQuestion,
1068
+ });
1061
1069
 
1062
1070
  return (
1063
1071
  <div className="agent-kit-chat-footer-stack">
1064
1072
  {questions?.length ? (
1065
1073
  <div className="agent-kit-chat-guided-question">
1066
- <GuidedQuestionFlow
1074
+ <GuidedQuestionCard
1067
1075
  questions={questions}
1068
1076
  onSubmit={handleSubmit}
1069
1077
  onSkip={handleSkip}
@@ -1074,7 +1082,6 @@ function ChatAgentFooter({ children }: { children: ReactNode }) {
1074
1082
  isSubmissionBlocked={isSubmissionBlocked}
1075
1083
  providerStatus={providerStatus}
1076
1084
  onRetryProviderStatus={retryProviderStatus}
1077
- className="h-auto items-stretch justify-stretch bg-transparent"
1078
1085
  />
1079
1086
  </div>
1080
1087
  ) : null}
@@ -83,11 +83,6 @@
83
83
  .agent-kit-chat-guided-question {
84
84
  max-block-size: min(32rem, 58dvh);
85
85
  overflow: auto;
86
- margin-block-end: 0.5rem;
87
- border: 1px solid var(--agentkit-border);
88
- border-radius: var(--agentkit-radius);
89
- background: var(--agentkit-surface);
90
- box-shadow: var(--agentkit-elevation);
91
86
  }
92
87
 
93
88
  .chat-sidebar-history .an-chat-history__list {
@@ -946,6 +946,22 @@ const disableDepSourcemapsPlugin = {
946
946
  name: "agent-native:no-dep-prebundle-sourcemaps",
947
947
  outputOptions: (options) => ({ ...options, sourcemap: false }),
948
948
  };
949
+ const disableServedDepSourcemapsPlugin = {
950
+ name: "agent-native:no-served-dep-sourcemaps",
951
+ apply: "serve",
952
+ enforce: "post",
953
+ transform(code, id) {
954
+ if (process.env.AGENT_NATIVE_DEP_SOURCEMAPS === "1" ||
955
+ this.environment.name !== "client" ||
956
+ this.environment.mode !== "dev" ||
957
+ !this.environment.depsOptimizer?.isOptimizedDepFile(id)) {
958
+ return;
959
+ }
960
+ // Vite injects an identity map when no map is present. It only takes the
961
+ // transformed map when the code changes, so add a trailing newline.
962
+ return { code: `${code}\n`, map: { mappings: "" } };
963
+ },
964
+ };
949
965
  function getDefaultOptimizeDeps(cwd) {
950
966
  const inMonorepo = findCoreSrcDir(cwd) !== null;
951
967
  const entries = [
@@ -3729,6 +3745,7 @@ function createAgentNativePlugins(options, { command, includeReactTransform, use
3729
3745
  clientOptionalPeerStubPlugin(cwd),
3730
3746
  ...userPlugins,
3731
3747
  externalStoreShimPlugin(),
3748
+ disableServedDepSourcemapsPlugin,
3732
3749
  appChangelogRawPlugin(),
3733
3750
  actionTypesPlugin(),
3734
3751
  agentsBundlePlugin({ agentNativeConfig: options.agentNativeConfig }),
@@ -109,7 +109,7 @@ Slack, email, Telegram, and WhatsApp all flow into Dispatch's agent loop. Connec
109
109
 
110
110
  ### Secret vault
111
111
 
112
- Store credentials once in Dispatch's vault. By default, vault access is **all apps**: every saved key is available to every workspace app, and `sync-vault-to-app` pushes the full vault to the target app. Workspaces that need stricter separation can switch the vault to **manual** mode, where explicit per-app grants are required before sync. Non-admins can **request** a secret for an app; admins **approve**, which creates the secret and, in manual workflows, the grant. Every read, grant, sync, and rotation is captured in an audit log. This is what makes "rotate the OpenAI key" a one-click operation across ten apps instead of ten PRs.
112
+ Store credentials once in Dispatch's vault. The value is encrypted into the shared credential store; the vault itself keeps only the key name, label, and grants, never a plaintext copy. By default, vault access is **all apps**: every saved key is available to every app that shares Dispatch's database. Workspaces that need stricter separation can switch the vault to **manual** mode, where explicit per-app grants are required. Non-admins can **request** a secret for an app; admins **approve**, which creates the secret and, in manual workflows, the grant. Every read, grant, and rotation is captured in an audit log. This is what makes "rotate the OpenAI key" a one-click operation across ten apps instead of ten PRs.
113
113
 
114
114
  ### Cross-app delegation
115
115
 
@@ -295,7 +295,7 @@ See [MCP Clients](/docs/mcp-clients) for the config schema, precedence rules, an
295
295
 
296
296
  The workspace root `.env` is loaded into every app automatically for deployment-level configuration such as `A2A_SECRET`, `BETTER_AUTH_SECRET`, and `DATABASE_URL`. User, org, and workspace API keys should be saved as scoped DB secrets instead of being written into `.env`. Per-app deploy-time overrides can still go in `apps/<name>/.env` and win on conflict.
297
297
 
298
- For runtime app credentials, prefer the Dispatch vault over hand-editing `.env` files. The vault defaults to all-apps access, so every saved vault key is available to every workspace app and can be pushed with `sync-vault-to-app`. Switch the vault to manual mode only when apps need explicit per-key grants.
298
+ For runtime app credentials, prefer the Dispatch vault over hand-editing `.env` files. The vault defaults to all-apps access, so every saved vault key is available to every workspace app that shares its database. Switch the vault to manual mode only when apps need explicit per-key grants.
299
299
 
300
300
  ```text
301
301
  my-company-platform/
@@ -322,7 +322,7 @@ pnpm action db-check-scoping --require-org # Also require org_id
322
322
  | OAuth tokens (Google, GitHub) | `oauth_tokens` store via `saveOAuthTokens()` |
323
323
  | Session tokens | Automatic (Better Auth handles this) |
324
324
 
325
- Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy combined `SECRETS_ENCRYPTION_KEY`, then material derived from `A2A_SECRET`, with `BETTER_AUTH_SECRET` and the app-scoped key retained as compatibility candidates. Successful fallback reads compare-and-swap re-encrypt the shared column under the preferred key. During a workspace-key rotation, set `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` to the old value until reads have migrated existing rows. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
325
+ Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. The Dispatch vault keeps no plaintext copy: `vault_secrets` holds names and metadata, and each value lives only in `app_secrets`. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy combined `SECRETS_ENCRYPTION_KEY`, then material derived from `A2A_SECRET`, with `BETTER_AUTH_SECRET` and the app-scoped key retained as compatibility candidates. Successful fallback reads compare-and-swap re-encrypt the shared column under the preferred key. During a workspace-key rotation, set `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` to the old value until reads have migrated existing rows. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
326
326
 
327
327
  Never store secrets in `settings`, `application_state`, source code, or action responses. Use the credential / vault APIs above — they handle both encryption and per-user scoping.
328
328
 
@@ -44,7 +44,7 @@ Dispatch ships close to a hundred actions. Most of the UI-facing ones are alread
44
44
  | `create-vault-secret` / `update-vault-secret` / `delete-vault-secret` | Manage a stored credential. |
45
45
  | `create-vault-grant` / `revoke-vault-grant` / `list-vault-grants` | Grant, revoke, or list per-app secret access. |
46
46
  | `request-vault-secret` / `approve-vault-request` / `deny-vault-request` / `list-vault-requests` | The request-and-review workflow for a new secret. |
47
- | `grant-vault-secrets-to-app` / `sync-vault-to-app` | Push access, or push the full vault, to one app. |
47
+ | `grant-vault-secrets-to-app` | Grant several vault keys to one app in manual mode. |
48
48
  | `list-vault-secrets` / `list-vault-secret-options` / `list-vault-audit` | Read secret metadata (never values) and the audit trail. |
49
49
  | `get-vault-access-settings` / `set-vault-access-settings` | Read or change the all-apps vs. manual access mode. |
50
50
 
@@ -230,7 +230,6 @@ For a new workspace, after running `npx --yes @agent-native/core@latest create`:
230
230
 
231
231
  - [ ] Add shared secrets to the vault (API keys, OAuth credentials, etc.)
232
232
  - [ ] Keep the default all-apps vault policy or switch to manual per-app grants
233
- - [ ] Sync vault secrets to push them to apps
234
233
  - [ ] Register reusable workspace connections for shared provider accounts, then
235
234
  grant apps such as Brain, Analytics, Mail, or Dispatch only when they need
236
235
  that account