Skip to content

Commit a7d1db7

Browse files
author
Shaw
committed
Merge remote-tracking branch 'origin/develop' into develop
2 parents 64964af + 149df1f commit a7d1db7

20 files changed

Lines changed: 629 additions & 114 deletions

File tree

packages/agent/scripts/build-mobile-bundle.mjs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,51 @@ await rm(outDir, { recursive: true, force: true });
6161
await mkdir(outDir, { recursive: true });
6262

6363
function findPgliteDist() {
64+
// pglite.wasm + pglite.data MUST match the @electric-sql/pglite version
65+
// that the bundled agent JS resolves at runtime — they're a triple
66+
// (engine + filesystem image + JS shim). The agent imports from
67+
// @elizaos/plugin-sql, which pins ^0.3.3, while the eliza repo's
68+
// top-level deps may pull in a newer 0.4.x for unrelated reasons.
69+
// Bun's hoisting can park a 0.4.x copy at the top of `node_modules/.bun`
70+
// and a `readdirSync` walk will pick that up first. The runtime then
71+
// throws "Invalid FS bundle size: <new> !== <old>" because the bundled
72+
// 0.3.x WASM expects the 0.3.x .data while we shipped 0.4.x.
73+
//
74+
// Resolve plugin-sql's OWN private node_modules first so the staged
75+
// assets always match the bundled engine. Fall back to the repoRoot
76+
// hoisted location and to the .bun cache for the bundled-monorepo
77+
// case where plugin-sql is hoisted instead of nested.
6478
const candidates = [
79+
path.join(
80+
repoRoot,
81+
"plugins",
82+
"plugin-sql",
83+
"typescript",
84+
"node_modules",
85+
"@electric-sql",
86+
"pglite",
87+
"dist",
88+
),
6589
path.join(repoRoot, "node_modules", "@electric-sql", "pglite", "dist"),
6690
];
6791
const bunDir = path.join(repoRoot, "node_modules", ".bun");
6892
if (existsSync(bunDir)) {
69-
for (const entry of readdirSyncSafe(bunDir)) {
70-
if (entry.startsWith("@electric-sql+pglite@")) {
71-
candidates.push(
72-
path.join(
73-
bunDir,
74-
entry,
75-
"node_modules",
76-
"@electric-sql",
77-
"pglite",
78-
"dist",
79-
),
80-
);
81-
}
93+
// Sort .bun entries by version so that 0.3.x wins over 0.4.x —
94+
// matches the plugin-sql ^0.3.3 pin without forcing a manual list.
95+
const sortedEntries = readdirSyncSafe(bunDir)
96+
.filter((e) => e.startsWith("@electric-sql+pglite@"))
97+
.sort();
98+
for (const entry of sortedEntries) {
99+
candidates.push(
100+
path.join(
101+
bunDir,
102+
entry,
103+
"node_modules",
104+
"@electric-sql",
105+
"pglite",
106+
"dist",
107+
),
108+
);
82109
}
83110
}
84111
for (const c of candidates) {

packages/agent/src/runtime/prompt-compaction.ts

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,19 @@ export function compactCodingExamplesForIntent(prompt: string): string {
289289
}
290290

291291
/**
292-
* Context-aware action formatting. Replaces the <actions>...</actions>
293-
* block in the prompt with a version where only intent-relevant actions
294-
* have full <params> — the rest are stubs with just name + description.
292+
* Context-aware action formatting. Replaces the available-actions block in
293+
* the prompt with a version where only intent-relevant actions keep full
294+
* parameter detail — the rest are stubs with just name + description.
295+
*
296+
* Supports two prompt encodings:
297+
* - TOON (current default): the actions provider emits
298+
* actions[N]:
299+
* - ACTION: description
300+
* aliases[..]: ...
301+
* tags[..]: ...
302+
* params[..]: ...
303+
* example: ...
304+
* - XML (legacy): <actions><action><name>..</name>...</action>...</actions>
295305
*
296306
* If no intents are detected (general chat), only universal actions
297307
* (REPLY, NONE, IGNORE) keep full params — all others are stubbed.
@@ -309,7 +319,116 @@ export function compactActionsForIntent(prompt: string): string {
309319
// are always preserved, so the LLM can still select the right action; it
310320
// just won't see detailed param schemas until the user triggers a known intent.
311321

312-
// Find the first <actions>...</actions> block (the Available Actions section)
322+
const intentCategories = detectIntentCategories(prompt);
323+
// When no specific intent is detected, it's general chat — only universal
324+
// actions (REPLY, NONE, IGNORE) need full detail. All other actions get
325+
// stubs so the LLM knows they exist but doesn't waste context on params.
326+
const fullParamActions = buildFullParamActionSet(intentCategories);
327+
328+
// Try TOON-format first (current default), then fall back to XML for
329+
// legacy prompts that still emit <actions>...</actions> blocks.
330+
const toonCompacted = compactToonActionsBlock(prompt, fullParamActions);
331+
if (toonCompacted !== null) return toonCompacted;
332+
return compactXmlActionsBlock(prompt, fullParamActions);
333+
}
334+
335+
/**
336+
* Locate and compact a TOON-formatted "Available Actions" block. Returns
337+
* `null` if no TOON block is found, so the caller can try XML.
338+
*/
339+
function compactToonActionsBlock(
340+
prompt: string,
341+
fullParamActions: Set<string>,
342+
): string | null {
343+
// The actions provider emits "actions[N]:\n- NAME: desc\n params[..]: ...".
344+
// Anchor on that header line.
345+
const headerRe = /^actions\[\d+\]:[ \t]*$/m;
346+
const headerMatch = headerRe.exec(prompt);
347+
if (!headerMatch) return null;
348+
349+
const blockStart = headerMatch.index;
350+
const headerLine = headerMatch[0];
351+
const bodyStart = blockStart + headerLine.length;
352+
353+
// Walk forward consuming action entries (`- NAME: ...`) and their
354+
// two-space-indented continuation lines. Stop at the first non-indented
355+
// non-entry line that isn't part of an entry.
356+
const remainder = prompt.slice(bodyStart);
357+
const lines = remainder.split("\n");
358+
359+
let consumed = 0;
360+
if (lines.length > 0 && lines[0] === "") {
361+
consumed = 1;
362+
}
363+
364+
while (consumed < lines.length) {
365+
const line = lines[consumed];
366+
if (line.startsWith("- ") || line.startsWith(" ")) {
367+
consumed += 1;
368+
continue;
369+
}
370+
if (line === "") {
371+
const next = lines[consumed + 1];
372+
if (next !== undefined && next.startsWith("- ")) {
373+
consumed += 1;
374+
continue;
375+
}
376+
break;
377+
}
378+
break;
379+
}
380+
381+
const bodyLines = lines.slice(0, consumed);
382+
const blockEnd = bodyStart + bodyLines.join("\n").length;
383+
384+
type ToonAction = { name: string; entryLines: string[] };
385+
const entries: ToonAction[] = [];
386+
let current: ToonAction | null = null;
387+
for (const line of bodyLines) {
388+
if (line.startsWith("- ")) {
389+
if (current) entries.push(current);
390+
const nameMatch = /^- ([A-Z0-9_]+):/.exec(line);
391+
const name = nameMatch?.[1] ?? "";
392+
current = { name, entryLines: [line] };
393+
} else if (current && (line.startsWith(" ") || line === "")) {
394+
current.entryLines.push(line);
395+
}
396+
}
397+
if (current) entries.push(current);
398+
399+
if (entries.length === 0) return null;
400+
401+
const compactedEntries = entries.map((entry) => {
402+
if (!entry.name || fullParamActions.has(entry.name)) {
403+
return entry.entryLines.join("\n");
404+
}
405+
// Stub: keep only `- NAME: description`; drop continuation lines.
406+
return entry.entryLines[0];
407+
});
408+
409+
while (
410+
compactedEntries.length > 0 &&
411+
compactedEntries[compactedEntries.length - 1] === ""
412+
) {
413+
compactedEntries.pop();
414+
}
415+
416+
const compactedBody = compactedEntries.join("\n");
417+
const before = prompt.slice(0, bodyStart);
418+
const after = prompt.slice(blockEnd);
419+
const separator = bodyLines.length > 0 && bodyLines[0] === "" ? "\n" : "";
420+
return `${before}${separator}${compactedBody}${after}`;
421+
}
422+
423+
/**
424+
* Legacy XML compaction: locate a `<actions>...</actions>` block and stub
425+
* non-relevant `<action>` entries. Returns the original prompt unchanged
426+
* when no XML block is present.
427+
*/
428+
function compactXmlActionsBlock(
429+
prompt: string,
430+
fullParamActions: Set<string>,
431+
): string {
313432
const actionsStart = prompt.indexOf("<actions>");
314433
if (actionsStart === -1) return prompt;
315434
const actionsEnd = prompt.indexOf("</actions>", actionsStart);
@@ -320,13 +439,6 @@ export function compactActionsForIntent(prompt: string): string {
320439
actionsEnd,
321440
);
322441

323-
const intentCategories = detectIntentCategories(prompt);
324-
// When no specific intent is detected, it's general chat — only universal
325-
// actions (REPLY, NONE, IGNORE) need full detail. All other actions get
326-
// stubs so the LLM knows they exist but doesn't waste context on params.
327-
const fullParamActions = buildFullParamActionSet(intentCategories);
328-
329-
// Parse individual <action>...</action> blocks
330442
const actionRegex = /<action>([\s\S]*?)<\/action>/g;
331443
const compactedActions: string[] = [];
332444

@@ -338,10 +450,8 @@ export function compactActionsForIntent(prompt: string): string {
338450
const actionName = nameMatch[1].trim();
339451

340452
if (fullParamActions.has(actionName)) {
341-
// Keep full action with params
342453
compactedActions.push(` <action>${actionInner}</action>`);
343454
} else {
344-
// Stub: name + description only, strip <params>
345455
const descMatch = actionInner.match(
346456
/<description>([\s\S]*?)<\/description>/,
347457
);

packages/app-core/platforms/android/app/src/main/java/ai/elizaos/app/ElizaAgentService.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.content.res.AssetManager;
1212
import android.os.Build;
1313
import android.os.IBinder;
14+
import android.provider.Settings;
1415
import android.util.Log;
1516

1617
import androidx.core.app.NotificationCompat;
@@ -808,6 +809,87 @@ private void startAgentProcess() {
808809
agentEnv.put("BUN_FEATURE_FLAG_FORCE_WAITER_THREAD", "1");
809810
agentEnv.put("BUN_FEATURE_FLAG_DISABLE_RWF_NONBLOCK", "1");
810811
agentEnv.put("BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH", "1");
812+
// BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER=1
813+
// Forces bun's transpiler to run on the main thread
814+
// instead of the async worker pool. The worker pool
815+
// uses pthread + futex_waitv (added in 5.16) which
816+
// Android's app seccomp policy blocks on most kernels
817+
// before API 34. Disables the worker thread spawn
818+
// entirely — the transpiler still runs, just inline.
819+
//
820+
// NOTE: Do NOT set BUN_FEATURE_FLAG_DISABLE_MEMFD=1 here.
821+
// memfd_create IS on Android's app seccomp allowlist
822+
// (verified API 30+), and bun's JSC tier uses memfd as
823+
// the W^X dual-mapping mechanism for JIT code pages.
824+
// Disabling memfd forces JSC to fall back to raw RWX
825+
// mmap, which IS blocked by SELinux execmem on platform_app
826+
// — that combination kills bun before any log line is
827+
// written. Tested empirically: with the 43 MB agent-bundle,
828+
// DISABLE_MEMFD=1 produces an early SIGSYS during JIT init;
829+
// with memfd allowed, bun reaches PGlite + listener.
830+
agentEnv.put("BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", "1");
831+
832+
// ── No on-device prompt-optimization / training ────────────
833+
//
834+
// The runtime ships with a trajectory-driven prompt-optimization
835+
// pipeline (MIPRO / GEPA / bootstrap-fewshot via the native
836+
// backend). On boot, OptimizedPromptService kicks off a one-
837+
// shot bootstrap when accumulated trajectories cross threshold,
838+
// and the cron auto-trainer dispatches further rounds in the
839+
// background. None of that belongs on a phone or a privileged
840+
// system app:
841+
// - MIPRO/GEPA spawn coding sub-agents (PTY-backed bash) that
842+
// blow past the bun seccomp envelope this service builds.
843+
// - The trajectory writer fans out to the trajectories table
844+
// under PGlite which already churns the device flash.
845+
// - On AOSP cvd we want a deterministic agent binary, not
846+
// one that mutates its prompts mid-smoke.
847+
//
848+
// Hard-disable both the bootstrap and the trajectory ingest
849+
// path so the agent never spins up a training round on-device.
850+
// Trajectories are still useful for live chat context, but
851+
// this disables PERSISTENCE — the optimizer has no input data
852+
// and no-ops at boot. Both env names are set: the source uses
853+
// ELIZA_* post-rename, the bundled training-trigger.js still
854+
// reads MILADY_* in older dist-mobile builds.
855+
agentEnv.put("ELIZA_DISABLE_AUTO_BOOTSTRAP", "1");
856+
agentEnv.put("MILADY_DISABLE_AUTO_BOOTSTRAP", "1");
857+
agentEnv.put("ELIZA_DISABLE_TRAJECTORY_LOGGING", "1");
858+
859+
// ── Vault passphrase ──────────────────────────────────────
860+
// The runtime's vault-bootstrap mirrors process.env secrets
861+
// through @elizaos/vault, which on a headless Linux host
862+
// (Android counts: no reachable D-Bus session) refuses the
863+
// OS keychain and demands ELIZA_VAULT_PASSPHRASE (≥12 chars)
864+
// to derive a master key. Without it the bootstrap fails
865+
// and startEliza() throws "[vault-bootstrap] all 1 secret
866+
// writes failed; vault unreachable", which the watchdog
867+
// interprets as a crash and restart-loops the agent.
868+
//
869+
// Derive a per-install stable passphrase from ANDROID_ID
870+
// (Settings.Secure.ANDROID_ID — 16 hex chars, per-app-install
871+
// on Android 8+, stable across reboots and OS updates).
872+
// Prefix with a constant so the value is always ≥12 chars
873+
// even if ANDROID_ID is unexpectedly short or null. The
874+
// resulting passphrase is opaque to the user and is only
875+
// ever stored in memory in the spawned bun process.
876+
//
877+
// Operators can override by setting ELIZA_VAULT_PASSPHRASE
878+
// in the parent service env (e.g. for a deterministic dev
879+
// passphrase across reinstalls).
880+
if (!env.containsKey("ELIZA_VAULT_PASSPHRASE")) {
881+
String androidId = Settings.Secure.getString(
882+
getContentResolver(),
883+
Settings.Secure.ANDROID_ID
884+
);
885+
if (androidId == null || androidId.length() < 8) {
886+
androidId = "fallback-" + Build.SERIAL;
887+
}
888+
agentEnv.put(
889+
"ELIZA_VAULT_PASSPHRASE",
890+
"elizaos-android-vault-" + androidId
891+
);
892+
}
811893

812894
// Default to info-level logging so plugin resolution + listen
813895
// progress is visible in agent.log. The runtime defaults to

packages/app-core/scripts/lib/stage-android-agent.mjs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import { spawn } from "node:child_process";
4040
import fs from "node:fs";
4141
import os from "node:os";
4242
import path from "node:path";
43+
import { fileURLToPath } from "node:url";
44+
45+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
4346

4447
const BUN_VERSION = "1.3.13";
4548
const ALPINE_BRANCH = "v3.21";
@@ -502,19 +505,50 @@ export async function stageAndroidAgentRuntime({
502505
// Mirror that by staging vector + fuzzystrmatch in the assets tree at
503506
// the same level as agent-bundle.js, leaving relative resolution alone.
504507
//
505-
// spikeDir is `<repoRoot>/scripts/spike-android-agent/`; its parent's
506-
// parent is the repo root. Resolve dist-mobile relative to that.
507-
// (Inside this repo there is no nested `eliza/` directory — that
508-
// prefix was a leftover from eliza's outer repo layout where eliza
509-
// was a submodule.)
510-
const distMobileDir = path.resolve(
508+
// The agent bundle is produced by `bun run --cwd packages/agent build:mobile`
509+
// and always lands in `<eliza-root>/packages/agent/dist-mobile/`. Resolve
510+
// it relative to THIS script's location (eliza/packages/app-core/scripts/lib/)
511+
// — that's a stable layout invariant. Resolving relative to spikeDir or
512+
// process.cwd() breaks when the eliza package is nested as a submodule
513+
// under a consumer repo (Eliza/Milady-style), because their `scripts/`
514+
// and `packages/` directories live one level OUT from the eliza checkout.
515+
//
516+
// The legacy fallback to `<repoRoot>/packages/agent/dist-mobile/` is kept
517+
// for the standalone-eliza-monorepo build path where this same script
518+
// also runs and the bundle sits at the consumer-repo root.
519+
const elizaPackagesAgentDistMobile = path.resolve(
520+
__dirname,
521+
"..", // scripts/
522+
"..", // app-core/
523+
"..", // packages/
524+
"agent",
525+
"dist-mobile",
526+
);
527+
const consumerPackagesAgentDistMobile = path.resolve(
511528
path.dirname(spikeDir),
512529
"..",
513530
"packages",
514531
"agent",
515532
"dist-mobile",
516533
);
517-
const distBundle = path.join(distMobileDir, "agent-bundle.js");
534+
const distMobileCandidates = [
535+
elizaPackagesAgentDistMobile,
536+
consumerPackagesAgentDistMobile,
537+
];
538+
let distMobileDir = null;
539+
let distBundle = null;
540+
for (const candidate of distMobileCandidates) {
541+
const bundle = path.join(candidate, "agent-bundle.js");
542+
if (fs.existsSync(bundle)) {
543+
distMobileDir = candidate;
544+
distBundle = bundle;
545+
break;
546+
}
547+
}
548+
if (!distBundle) {
549+
distMobileDir = elizaPackagesAgentDistMobile;
550+
distBundle = path.join(distMobileDir, "agent-bundle.js");
551+
}
518552
const spikeServerJs = path.join(spikeDir, "server.js");
519553

520554
let bundleSrc;

0 commit comments

Comments
 (0)