@agent-native/core 0.101.11 → 0.101.12

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.
@@ -1,7 +1,9 @@
1
1
  import { ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER } from "../action-ui.js";
2
2
  import { writeAppState } from "../application-state/script-helpers.js";
3
- import { getRequestRunContext } from "../server/request-context.js";
3
+ import { readResource } from "../resources/script-helpers.js";
4
+ import { getRequestOrgId, getRequestRunContext, getRequestUserEmail, } from "../server/request-context.js";
4
5
  import { resolveAccess } from "../sharing/access.js";
6
+ import { readWorkspaceFile, } from "../workspace-files/store.js";
5
7
  import { getLocalExtension, isLocalExtensionRow, listLocalExtensions, } from "./local.js";
6
8
  import { extensionPath } from "./path.js";
7
9
  import { addExtensionSlotTarget, installExtensionSlot, uninstallExtensionSlot, listExtensionsForSlot, listSlotsForExtension, } from "./slots/store.js";
@@ -392,7 +394,7 @@ export function createExtensionActionEntries() {
392
394
  },
393
395
  "create-extension": {
394
396
  tool: {
395
- description: 'Create a persisted sandboxed Alpine.js mini-app extension and render it inline in the chat. Use this when the user wants generated UI that should be saved, reusable, or visible in the Extensions view: extensions, widgets, dashboards, calculators, mini-apps, and reusable interactive utilities. For one-time chat-only UI, use render-inline-extension instead. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use appAction() for app data writes and extensionData for extension-owned persisted UI state; dbQuery() is for read-only inspection of known app SQL tables. Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. Persist reusable user-edited state with extensionData: if the extension has checkboxes, todos, notes, filters, preferences, or any control whose value should survive reload/reopen, load that state on init and save changes with extensionData, usually at user scope, instead of keeping it only in Alpine state. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery() only for known existing SQL tables and never for writes. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous initial tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
397
+ description: 'Create a persisted sandboxed Alpine.js mini-app extension and render it inline in the chat. Use this when the user wants generated UI that should be saved, reusable, or visible in the Extensions view: extensions, widgets, dashboards, calculators, mini-apps, and reusable interactive utilities. For one-time chat-only UI, use render-inline-extension instead. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use appAction() for app data writes and extensionData for extension-owned persisted UI state; dbQuery() is for read-only inspection of known app SQL tables. Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. Persist reusable user-edited state with extensionData: if the extension has checkboxes, todos, notes, filters, preferences, or any control whose value should survive reload/reopen, load that state on init and save changes with extensionData, usually at user scope, instead of keeping it only in Alpine state. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. IMPORTANT — cloning a large extension that lives as a workspace resource (not a chat attachment): leave `content` empty and pass `contentFromWorkspaceFile` set to the resource path (e.g. "intuit-analytics-extension.html"); the server reads the full file. Do NOT try to reconstruct the body with run-code or route create-extension through run-code (mutating actions are not callable there). Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery() only for known existing SQL tables and never for writes. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous initial tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
396
398
  parameters: {
397
399
  type: "object",
398
400
  properties: {
@@ -412,6 +414,10 @@ export function createExtensionActionEntries() {
412
414
  type: "string",
413
415
  description: 'Host a pasted/attached file verbatim WITHOUT re-typing it. Set this to the name of an attachment on the current turn (e.g. "pasted-text-1718000000000-ab12cd.txt") or the literal "latest" for the most recent pasted block; the server resolves it into the extension content. Use this instead of `content` whenever the user pasted a large file to host — it avoids re-emitting thousands of tokens. When set, leave `content` empty.',
414
416
  },
417
+ contentFromWorkspaceFile: {
418
+ type: "string",
419
+ description: 'Host a workspace/shared resource file verbatim WITHOUT re-typing it. Set this to the resource path (e.g. "intuit-analytics-extension.html"); the server reads the full file and uses it as the extension content. Use this — NOT run-code or contentFromAttachment — when cloning a large extension body that already exists as a workspace resource. When set, leave `content` empty.',
420
+ },
415
421
  icon: {
416
422
  type: "string",
417
423
  description: "Optional icon name or short label.",
@@ -428,7 +434,7 @@ export function createExtensionActionEntries() {
428
434
  const name = String(args?.name ?? "").trim();
429
435
  if (!name)
430
436
  return "Error: name is required.";
431
- const resolved = resolveExtensionContent(args, ctx);
437
+ const resolved = await resolveExtensionContentAsync(args, ctx);
432
438
  if ("error" in resolved)
433
439
  return resolved.error;
434
440
  const content = resolved.content.trim();
@@ -462,9 +468,12 @@ export function createExtensionActionEntries() {
462
468
  catch {
463
469
  // Non-fatal — agent can still mention the path in its reply.
464
470
  }
471
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
465
472
  return {
466
473
  ok: true,
467
- extension: { ...existing, path: existingPath },
474
+ // Compact summary (contentLength + contentHash, no full body). Echoing
475
+ // the whole HTML back is pure token waste — the agent just supplied it.
476
+ extension: await summarizeExtension(existing, hiddenIds, false),
468
477
  path: existingPath,
469
478
  next: `Extension was already created in this session (recovered from a connection retry). The user is being navigated to it — no further navigation tool calls needed.`,
470
479
  };
@@ -492,9 +501,12 @@ export function createExtensionActionEntries() {
492
501
  catch {
493
502
  // Non-fatal — agent can still mention the path in its reply.
494
503
  }
504
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
495
505
  return {
496
506
  ok: true,
497
- extension: { ...extension, path },
507
+ // Compact summary (contentLength + contentHash, no full body). Echoing
508
+ // the whole HTML back is pure token waste — the agent just supplied it.
509
+ extension: await summarizeExtension(extension, hiddenIds, false),
498
510
  path,
499
511
  next: `Created. The user is being navigated to the new extension automatically — no further navigation tool calls needed.`,
500
512
  };
@@ -526,6 +538,10 @@ export function createExtensionActionEntries() {
526
538
  type: "string",
527
539
  description: 'Optional full replacement sourced from a pasted/attached file on the current turn, by attachment name (or the literal "latest" for the most recent pasted block). Use instead of `content` when replacing the whole body with a large pasted file so you do not have to re-type it. Ignored when `content` is provided.',
528
540
  },
541
+ contentFromWorkspaceFile: {
542
+ type: "string",
543
+ description: 'Optional full replacement sourced from a workspace/shared resource file, by resource path (e.g. "intuit-analytics-extension.html"). The server reads the full file and uses it as the replacement body. Use instead of `content` when replacing the whole body with a large file that exists as a workspace resource. Ignored when `content` is provided.',
544
+ },
529
545
  patches: {
530
546
  type: "string",
531
547
  description: 'Legacy optional JSON array of { "find": "...", "replace": "...", "all"?: true, "expectedMatches"?: 1, "required"?: true } patches. Missing required targets fail instead of silently no-oping.',
@@ -568,8 +584,9 @@ export function createExtensionActionEntries() {
568
584
  ? args.content
569
585
  : undefined;
570
586
  if (replacementContent === undefined &&
571
- args?.contentFromAttachment !== undefined) {
572
- const resolved = resolveExtensionContent(args, ctx);
587
+ (args?.contentFromAttachment !== undefined ||
588
+ args?.contentFromWorkspaceFile !== undefined)) {
589
+ const resolved = await resolveExtensionContentAsync(args, ctx);
573
590
  if ("error" in resolved)
574
591
  return resolved.error;
575
592
  replacementContent = resolved.content;
@@ -1154,6 +1171,111 @@ function resolveExtensionContent(args, ctx) {
1154
1171
  }
1155
1172
  return { content: resolved };
1156
1173
  }
1174
+ /**
1175
+ * Resolve the workspace-files bridge scope exactly the way run-code's
1176
+ * workspaceRead/workspaceWrite do: org-preferred (org → shared owner) with the
1177
+ * requesting user's email as the solo fallback. Kept in lockstep with
1178
+ * `resolveScope` in `workspace-files/tool.ts`.
1179
+ */
1180
+ function workspaceFilesBridgeScope() {
1181
+ const orgId = getRequestOrgId();
1182
+ if (orgId)
1183
+ return { scope: "org", scopeId: orgId };
1184
+ const email = getRequestUserEmail();
1185
+ if (email)
1186
+ return { scope: "user", scopeId: email };
1187
+ return null;
1188
+ }
1189
+ /**
1190
+ * Read a workspace/shared/personal resource file's FULL content by path.
1191
+ *
1192
+ * Precedence (single, documented rule so this never silently resolves a
1193
+ * different file than the agent inspected):
1194
+ * 1. The run-code `workspace-files` bridge scope (org → shared owner, else the
1195
+ * user's email). This is the SAME owner/scope `workspaceRead` /
1196
+ * `workspaceWrite` use, so a body the agent staged via `workspaceWrite` is
1197
+ * resolved here verbatim — the two paths cannot diverge.
1198
+ * 2. User-managed Resources (personal override → org/shared → workspace
1199
+ * default) as a fallback, for pre-built resources that were created in the
1200
+ * Resources panel rather than staged through the bridge.
1201
+ *
1202
+ * Unlike attachments, resource content is not capped/truncated on the way in, so
1203
+ * this is the correct path for cloning a large extension body that already
1204
+ * exists as a workspace resource (e.g. a per-customer dashboard).
1205
+ */
1206
+ async function readWorkspaceFileContent(path) {
1207
+ const trimmed = path.trim();
1208
+ if (!trimmed)
1209
+ return null;
1210
+ // 1) Bridge parity — resolve exactly the file workspaceRead/workspaceWrite see.
1211
+ const bridgeScope = workspaceFilesBridgeScope();
1212
+ if (bridgeScope) {
1213
+ let bridgeFile;
1214
+ try {
1215
+ bridgeFile = await readWorkspaceFile(bridgeScope, trimmed);
1216
+ }
1217
+ catch {
1218
+ // A THROW here is a transient store error or invalid path — NOT a
1219
+ // definitive "not found". Fail closed rather than silently hosting a
1220
+ // possibly-different same-path Resources body than workspaceRead
1221
+ // inspected. A retry re-runs this read cleanly.
1222
+ return null;
1223
+ }
1224
+ // A null result means the file genuinely does not exist in the bridge scope;
1225
+ // fall through to user-managed Resources for pre-built resource-panel files.
1226
+ if (bridgeFile && typeof bridgeFile.content === "string") {
1227
+ return bridgeFile.content;
1228
+ }
1229
+ }
1230
+ // 2) Fallback — user-managed Resources by scope precedence.
1231
+ for (const scope of ["personal", "shared", "workspace"]) {
1232
+ try {
1233
+ const content = await readResource(trimmed, { scope });
1234
+ if (typeof content === "string")
1235
+ return content;
1236
+ }
1237
+ catch {
1238
+ // A given scope can throw (e.g. `personal` when no user identity is
1239
+ // resolvable in this context). Don't let one scope abort the lookup —
1240
+ // fall through and try the next one.
1241
+ }
1242
+ }
1243
+ return null;
1244
+ }
1245
+ /**
1246
+ * Resolve the extension HTML body from (in priority order) inline `content`, a
1247
+ * `contentFromWorkspaceFile` resource path, or a `contentFromAttachment` handle.
1248
+ *
1249
+ * The workspace-file path exists because a large extension body frequently lives
1250
+ * as a workspace resource (not a chat attachment). Without it the model has no
1251
+ * viable route — inline is too large to shuttle reliably, contentFromAttachment
1252
+ * only sees chat attachments, and mutating actions cannot run from run-code — so
1253
+ * it loops and the run aborts with no_progress.
1254
+ */
1255
+ async function resolveExtensionContentAsync(args, ctx) {
1256
+ const inline = args?.content !== undefined ? String(args.content) : undefined;
1257
+ if (inline !== undefined && inline.trim().length > 0) {
1258
+ return { content: inline };
1259
+ }
1260
+ const wsRef = args?.contentFromWorkspaceFile !== undefined
1261
+ ? String(args.contentFromWorkspaceFile).trim()
1262
+ : "";
1263
+ if (wsRef) {
1264
+ const content = await readWorkspaceFileContent(wsRef);
1265
+ if (content === null) {
1266
+ return {
1267
+ error: `Error: contentFromWorkspaceFile="${wsRef}" did not match any readable workspace/shared/personal resource file. Check the exact path (e.g. "intuit-analytics-extension.html"), or pass the HTML inline via content.`,
1268
+ };
1269
+ }
1270
+ if (content.trim().length === 0) {
1271
+ return {
1272
+ error: `Error: workspace file "${wsRef}" is empty. Pass non-empty HTML inline via content, or point contentFromWorkspaceFile at a file with content.`,
1273
+ };
1274
+ }
1275
+ return { content };
1276
+ }
1277
+ return resolveExtensionContent(args, ctx);
1278
+ }
1157
1279
  function coerceBoolean(value) {
1158
1280
  return value === true || value === "true";
1159
1281
  }