@beeos-ai/device-agent 0.15.0 → 0.16.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 (240) hide show
  1. package/README.md +35 -0
  2. package/dist/adapters/agent-gateway-client.d.ts +6 -68
  3. package/dist/adapters/agent-gateway-client.js +7 -67
  4. package/dist/adapters/agent-gateway-client.js.map +1 -1
  5. package/dist/adapters/device-compat.d.ts +1 -2
  6. package/dist/adapters/device-compat.js +2 -52
  7. package/dist/adapters/device-compat.js.map +1 -1
  8. package/dist/agent-runtime/action-guard.js +92 -17
  9. package/dist/agent-runtime/action-guard.js.map +1 -1
  10. package/dist/agent-runtime/adapters/base.d.ts +13 -9
  11. package/dist/agent-runtime/adapters/base.js.map +1 -1
  12. package/dist/agent-runtime/adapters/general-flash.d.ts +3 -3
  13. package/dist/agent-runtime/adapters/general-flash.js +2 -2
  14. package/dist/agent-runtime/adapters/general-flash.js.map +1 -1
  15. package/dist/agent-runtime/adapters/native-tools.d.ts +56 -0
  16. package/dist/agent-runtime/adapters/native-tools.js +416 -0
  17. package/dist/agent-runtime/adapters/native-tools.js.map +1 -0
  18. package/dist/agent-runtime/adapters/openai.d.ts +1 -2
  19. package/dist/agent-runtime/adapters/openai.js +5 -5
  20. package/dist/agent-runtime/adapters/openai.js.map +1 -1
  21. package/dist/agent-runtime/adapters/pro.d.ts +7 -37
  22. package/dist/agent-runtime/adapters/pro.js +7 -249
  23. package/dist/agent-runtime/adapters/pro.js.map +1 -1
  24. package/dist/agent-runtime/advanced/app-card.js +9 -2
  25. package/dist/agent-runtime/advanced/app-card.js.map +1 -1
  26. package/dist/agent-runtime/advanced/subgoal-verifier.d.ts +1 -0
  27. package/dist/agent-runtime/advanced/subgoal-verifier.js +30 -36
  28. package/dist/agent-runtime/advanced/subgoal-verifier.js.map +1 -1
  29. package/dist/agent-runtime/advanced/task-state.js +4 -9
  30. package/dist/agent-runtime/advanced/task-state.js.map +1 -1
  31. package/dist/agent-runtime/agent-runner.d.ts +2 -11
  32. package/dist/agent-runtime/agent-runner.js +69 -34
  33. package/dist/agent-runtime/agent-runner.js.map +1 -1
  34. package/dist/agent-runtime/core.d.ts +24 -46
  35. package/dist/agent-runtime/core.js +161 -228
  36. package/dist/agent-runtime/core.js.map +1 -1
  37. package/dist/agent-runtime/execution-checks.d.ts +40 -0
  38. package/dist/agent-runtime/execution-checks.js +133 -0
  39. package/dist/agent-runtime/execution-checks.js.map +1 -0
  40. package/dist/agent-runtime/execution-mode.d.ts +1 -8
  41. package/dist/agent-runtime/execution-mode.js +10 -30
  42. package/dist/agent-runtime/execution-mode.js.map +1 -1
  43. package/dist/agent-runtime/feedback.d.ts +4 -28
  44. package/dist/agent-runtime/feedback.js +4 -26
  45. package/dist/agent-runtime/feedback.js.map +1 -1
  46. package/dist/agent-runtime/form-prerequisites.d.ts +3 -0
  47. package/dist/agent-runtime/form-prerequisites.js +52 -0
  48. package/dist/agent-runtime/form-prerequisites.js.map +1 -0
  49. package/dist/agent-runtime/index.d.ts +2 -8
  50. package/dist/agent-runtime/index.js +2 -8
  51. package/dist/agent-runtime/index.js.map +1 -1
  52. package/dist/agent-runtime/{modes/orchestrator → knowledge}/app-guide.d.ts +1 -1
  53. package/dist/agent-runtime/{modes/orchestrator → knowledge}/app-guide.js +1 -1
  54. package/dist/agent-runtime/knowledge/app-guide.js.map +1 -0
  55. package/dist/agent-runtime/models/base.d.ts +3 -1
  56. package/dist/agent-runtime/models/client.js +30 -9
  57. package/dist/agent-runtime/models/client.js.map +1 -1
  58. package/dist/agent-runtime/models/reasoning.d.ts +10 -0
  59. package/dist/agent-runtime/models/reasoning.js +29 -0
  60. package/dist/agent-runtime/models/reasoning.js.map +1 -0
  61. package/dist/agent-runtime/models/runtime-registry.d.ts +2 -1
  62. package/dist/agent-runtime/models/runtime-registry.js +4 -2
  63. package/dist/agent-runtime/models/runtime-registry.js.map +1 -1
  64. package/dist/agent-runtime/models/vlm/autoglm.js +6 -2
  65. package/dist/agent-runtime/models/vlm/autoglm.js.map +1 -1
  66. package/dist/agent-runtime/models/vlm/base.js +1 -12
  67. package/dist/agent-runtime/models/vlm/base.js.map +1 -1
  68. package/dist/agent-runtime/models/vlm/index.d.ts +0 -4
  69. package/dist/agent-runtime/models/vlm/index.js +0 -4
  70. package/dist/agent-runtime/models/vlm/index.js.map +1 -1
  71. package/dist/agent-runtime/modes/hybrid/index.d.ts +2 -12
  72. package/dist/agent-runtime/modes/hybrid/index.js +10 -27
  73. package/dist/agent-runtime/modes/hybrid/index.js.map +1 -1
  74. package/dist/agent-runtime/modes/hybrid/planner/base.d.ts +4 -44
  75. package/dist/agent-runtime/modes/hybrid/planner/base.js +3 -21
  76. package/dist/agent-runtime/modes/hybrid/planner/base.js.map +1 -1
  77. package/dist/agent-runtime/modes/hybrid/planner/llm-planner.d.ts +1 -1
  78. package/dist/agent-runtime/modes/hybrid/planner/llm-planner.js +9 -6
  79. package/dist/agent-runtime/modes/hybrid/planner/llm-planner.js.map +1 -1
  80. package/dist/agent-runtime/modes/hybrid/planner/prompts.d.ts +1 -17
  81. package/dist/agent-runtime/modes/hybrid/planner/prompts.js +65 -160
  82. package/dist/agent-runtime/modes/hybrid/planner/prompts.js.map +1 -1
  83. package/dist/agent-runtime/plan-executor.d.ts +13 -14
  84. package/dist/agent-runtime/plan-executor.js +236 -122
  85. package/dist/agent-runtime/plan-executor.js.map +1 -1
  86. package/dist/agent-runtime/pro-memory.js +1 -1
  87. package/dist/agent-runtime/pro-memory.js.map +1 -1
  88. package/dist/agent-runtime/pro-operator.js +8 -2
  89. package/dist/agent-runtime/pro-operator.js.map +1 -1
  90. package/dist/agent-runtime/pro-perception.d.ts +1 -0
  91. package/dist/agent-runtime/pro-perception.js +4 -2
  92. package/dist/agent-runtime/pro-perception.js.map +1 -1
  93. package/dist/agent-runtime/pro-target-check.d.ts +2 -2
  94. package/dist/agent-runtime/pro-target-check.js +102 -17
  95. package/dist/agent-runtime/pro-target-check.js.map +1 -1
  96. package/dist/agent-runtime/prompts/operator.d.ts +2 -0
  97. package/dist/agent-runtime/prompts/operator.js +69 -0
  98. package/dist/agent-runtime/prompts/operator.js.map +1 -0
  99. package/dist/agent-runtime/prompts/verification.d.ts +2 -0
  100. package/dist/agent-runtime/prompts/verification.js +52 -0
  101. package/dist/agent-runtime/prompts/verification.js.map +1 -0
  102. package/dist/agent-runtime/skills/replay.d.ts +4 -14
  103. package/dist/agent-runtime/skills/replay.js +46 -27
  104. package/dist/agent-runtime/skills/replay.js.map +1 -1
  105. package/dist/agent-runtime/tool-batch.d.ts +10 -0
  106. package/dist/agent-runtime/tool-batch.js +51 -0
  107. package/dist/agent-runtime/tool-batch.js.map +1 -0
  108. package/dist/agent-runtime/visual-state.d.ts +2 -0
  109. package/dist/agent-runtime/visual-state.js +24 -0
  110. package/dist/agent-runtime/visual-state.js.map +1 -0
  111. package/dist/agent-runtime/vlm.d.ts +1 -1
  112. package/dist/agent-runtime/vlm.js +1 -1
  113. package/dist/api.js +3 -1
  114. package/dist/api.js.map +1 -1
  115. package/dist/app/agent-host.js +6 -2
  116. package/dist/app/agent-host.js.map +1 -1
  117. package/dist/app/multi-device-host.js +4 -5
  118. package/dist/app/multi-device-host.js.map +1 -1
  119. package/dist/app/session-manager.d.ts +3 -45
  120. package/dist/app/session-manager.js +8 -9
  121. package/dist/app/session-manager.js.map +1 -1
  122. package/dist/bootstrap.js +25 -84
  123. package/dist/bootstrap.js.map +1 -1
  124. package/dist/config/loader.js +12 -1
  125. package/dist/config/loader.js.map +1 -1
  126. package/dist/config/schema.d.ts +7 -2
  127. package/dist/config/schema.js +8 -5
  128. package/dist/config/schema.js.map +1 -1
  129. package/dist/core/abortable-read.d.ts +1 -0
  130. package/dist/core/abortable-read.js +24 -0
  131. package/dist/core/abortable-read.js.map +1 -0
  132. package/dist/device/ui-xml.d.ts +4 -0
  133. package/dist/device/ui-xml.js +35 -13
  134. package/dist/device/ui-xml.js.map +1 -1
  135. package/dist/fleet/adapters/process/node-process-manager.d.ts +2 -1
  136. package/dist/fleet/adapters/process/node-process-manager.js +11 -19
  137. package/dist/fleet/adapters/process/node-process-manager.js.map +1 -1
  138. package/dist/inbound/acp/handlers/models.js +11 -8
  139. package/dist/inbound/acp/handlers/models.js.map +1 -1
  140. package/dist/inbound/acp/handlers/session.d.ts +1 -4
  141. package/dist/inbound/acp/handlers/session.js +3 -6
  142. package/dist/inbound/acp/handlers/session.js.map +1 -1
  143. package/dist/inbound/bma/agent.d.ts +47 -0
  144. package/dist/inbound/bma/agent.js +130 -0
  145. package/dist/inbound/bma/agent.js.map +1 -0
  146. package/dist/inbound/bma/command.d.ts +15 -0
  147. package/dist/inbound/bma/command.js +98 -0
  148. package/dist/inbound/bma/command.js.map +1 -0
  149. package/dist/inbound/bma/model-proxy.d.ts +22 -0
  150. package/dist/inbound/bma/model-proxy.js +130 -0
  151. package/dist/inbound/bma/model-proxy.js.map +1 -0
  152. package/dist/inbound/cli/main.js +12 -2
  153. package/dist/inbound/cli/main.js.map +1 -1
  154. package/dist/inbound/cli/run-artifacts.js +16 -7
  155. package/dist/inbound/cli/run-artifacts.js.map +1 -1
  156. package/dist/inbound/cli/run-command.d.ts +7 -0
  157. package/dist/inbound/cli/run-command.js +37 -29
  158. package/dist/inbound/cli/run-command.js.map +1 -1
  159. package/dist/index.d.ts +1 -10
  160. package/dist/index.js +1 -6
  161. package/dist/index.js.map +1 -1
  162. package/dist/mcp-client/autospawn.d.ts +7 -9
  163. package/dist/mcp-client/autospawn.js +16 -12
  164. package/dist/mcp-client/autospawn.js.map +1 -1
  165. package/dist/mcp-client/client.d.ts +6 -3
  166. package/dist/mcp-client/client.js +31 -13
  167. package/dist/mcp-client/client.js.map +1 -1
  168. package/dist/mcp-client/runtime.d.ts +5 -2
  169. package/dist/plugins/agent-sync/index.d.ts +32 -17
  170. package/dist/plugins/agent-sync/index.js +223 -232
  171. package/dist/plugins/agent-sync/index.js.map +1 -1
  172. package/dist/plugins/message-bus/inbound.d.ts +0 -32
  173. package/dist/plugins/message-bus/inbound.js +3 -0
  174. package/dist/plugins/message-bus/inbound.js.map +1 -1
  175. package/dist/plugins/message-bus/ms-channel-notifier.d.ts +1 -0
  176. package/dist/plugins/message-bus/ms-channel-notifier.js +16 -8
  177. package/dist/plugins/message-bus/ms-channel-notifier.js.map +1 -1
  178. package/dist/plugins/native-chat/deadline.d.ts +2 -0
  179. package/dist/plugins/native-chat/deadline.js +16 -0
  180. package/dist/plugins/native-chat/deadline.js.map +1 -0
  181. package/dist/plugins/native-chat/index.d.ts +46 -0
  182. package/dist/plugins/native-chat/index.js +331 -0
  183. package/dist/plugins/native-chat/index.js.map +1 -0
  184. package/dist/plugins/native-chat/reply-stream.d.ts +50 -0
  185. package/dist/plugins/native-chat/reply-stream.js +81 -0
  186. package/dist/plugins/native-chat/reply-stream.js.map +1 -0
  187. package/dist/plugins/native-chat/session-notifier.d.ts +16 -0
  188. package/dist/plugins/native-chat/session-notifier.js +130 -0
  189. package/dist/plugins/native-chat/session-notifier.js.map +1 -0
  190. package/dist/ports/agent-core.d.ts +2 -0
  191. package/dist/ports/agent-host.d.ts +2 -0
  192. package/dist/ports/agent-host.js +0 -21
  193. package/dist/ports/agent-host.js.map +1 -1
  194. package/dist/ports/device.d.ts +0 -1
  195. package/dist/ports/device.js +0 -1
  196. package/dist/ports/device.js.map +1 -1
  197. package/dist/runtime-authority/device-prompt.gen.d.ts +54 -0
  198. package/dist/runtime-authority/device-prompt.gen.js +19 -0
  199. package/dist/runtime-authority/device-prompt.gen.js.map +1 -0
  200. package/dist/types.d.ts +10 -0
  201. package/dist/types.js +1 -0
  202. package/dist/types.js.map +1 -1
  203. package/package.json +5 -3
  204. package/dist/adapters/device-desktop.d.ts +0 -49
  205. package/dist/adapters/device-desktop.js +0 -132
  206. package/dist/adapters/device-desktop.js.map +0 -1
  207. package/dist/agent-runtime/factory.d.ts +0 -24
  208. package/dist/agent-runtime/factory.js +0 -25
  209. package/dist/agent-runtime/factory.js.map +0 -1
  210. package/dist/agent-runtime/models/vlm/guiowl.d.ts +0 -1
  211. package/dist/agent-runtime/models/vlm/guiowl.js +0 -249
  212. package/dist/agent-runtime/models/vlm/guiowl.js.map +0 -1
  213. package/dist/agent-runtime/models/vlm/maiui.d.ts +0 -1
  214. package/dist/agent-runtime/models/vlm/maiui.js +0 -142
  215. package/dist/agent-runtime/models/vlm/maiui.js.map +0 -1
  216. package/dist/agent-runtime/models/vlm/qwen38.d.ts +0 -1
  217. package/dist/agent-runtime/models/vlm/qwen38.js +0 -241
  218. package/dist/agent-runtime/models/vlm/qwen38.js.map +0 -1
  219. package/dist/agent-runtime/models/vlm/qwenvl.d.ts +0 -1
  220. package/dist/agent-runtime/models/vlm/qwenvl.js +0 -202
  221. package/dist/agent-runtime/models/vlm/qwenvl.js.map +0 -1
  222. package/dist/agent-runtime/modes/orchestrator/app-guide.js.map +0 -1
  223. package/dist/agent-runtime/modes/orchestrator/index.d.ts +0 -41
  224. package/dist/agent-runtime/modes/orchestrator/index.js +0 -218
  225. package/dist/agent-runtime/modes/orchestrator/index.js.map +0 -1
  226. package/dist/agent-runtime/modes/orchestrator/prompt.d.ts +0 -7
  227. package/dist/agent-runtime/modes/orchestrator/prompt.js +0 -53
  228. package/dist/agent-runtime/modes/orchestrator/prompt.js.map +0 -1
  229. package/dist/agent-runtime/reflection.d.ts +0 -100
  230. package/dist/agent-runtime/reflection.js +0 -241
  231. package/dist/agent-runtime/reflection.js.map +0 -1
  232. package/dist/agent-runtime/reflectors/action-reflector.d.ts +0 -97
  233. package/dist/agent-runtime/reflectors/action-reflector.js +0 -234
  234. package/dist/agent-runtime/reflectors/action-reflector.js.map +0 -1
  235. package/dist/agent-runtime/reflectors/policy.d.ts +0 -56
  236. package/dist/agent-runtime/reflectors/policy.js +0 -111
  237. package/dist/agent-runtime/reflectors/policy.js.map +0 -1
  238. package/dist/agent-runtime/reflectors/trajectory-reflector.d.ts +0 -93
  239. package/dist/agent-runtime/reflectors/trajectory-reflector.js +0 -224
  240. package/dist/agent-runtime/reflectors/trajectory-reflector.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * agent-runtime/core — Plan-Act-Reflect loop.
2
+ * Android observation, prediction, execution and deterministic checks.
3
3
  *
4
4
  * Mirrors the Python reference at
5
5
  * /Users/heat/workspace/deviceagent_research/deviceagent/agent_runtime/loop/core.py
@@ -34,10 +34,14 @@ import { proCalls } from "./adapters/pro.js";
34
34
  import { buildProFrame } from "./pro-perception.js";
35
35
  import { isTerminal } from "@beeos-ai/device-common";
36
36
  import { formatFeedbackForPrompt } from "./feedback.js";
37
- import { HierarchicalReflector } from "./reflection.js";
37
+ import { ToolBatch } from "./tool-batch.js";
38
+ import { ExecutionChecks } from "./execution-checks.js";
38
39
  import { renderProOperatorContext } from "./pro-operator.js";
39
40
  import { checkProPerception } from "./pro-target-check.js";
41
+ import { compactUiXmlEvidence } from "../device/ui-xml.js";
42
+ import { visualStateHash } from "./visual-state.js";
40
43
  import { waitForAction } from "../core/wait.js";
44
+ import { abortableRead } from "../core/abortable-read.js";
41
45
  import { ProTaskMemory } from "./pro-memory.js";
42
46
  import { configuredOcrProvider } from "./ocr.js";
43
47
  import { resolveObservationMode, } from "./observation-mode.js";
@@ -46,18 +50,19 @@ import { resolveObservationMode, } from "./observation-mode.js";
46
50
  /* ──────────────────────────────────────────────────────────────────────── */
47
51
  export class AgentLoop {
48
52
  opts;
53
+ batch = new ToolBatch();
49
54
  static DEFAULT_MAX_STEPS = 30;
50
55
  static DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
51
56
  history = [];
52
57
  evidence;
58
+ observationIndex;
53
59
  proMemory;
54
- reflector;
60
+ executionChecks;
55
61
  notes = [];
56
- trajectoryVerdict;
57
62
  /**
58
63
  * One-way bus for diagnostics that need to surface in the next
59
64
  * `Observation.task` prefix. Drained right before each predict; entries
60
- * are pushed by veto / act_failed / trajectory-reflector paths. See
65
+ * are pushed by veto and act_failed paths. See
61
66
  * `feedback.ts` for the rationale (closes the reflection feedback loop
62
67
  * without adapter protocol changes).
63
68
  */
@@ -79,9 +84,12 @@ export class AgentLoop {
79
84
  * four identical actions.
80
85
  */
81
86
  vetoCodeCount = {};
87
+ signal;
88
+ deadline = Infinity;
89
+ timeoutReason = new Error("Task deadline reached");
82
90
  constructor(opts) {
83
91
  this.opts = opts;
84
- this.reflector = opts.reflector ?? new HierarchicalReflector(opts.adapter);
92
+ this.executionChecks = opts.executionChecks ?? new ExecutionChecks();
85
93
  this.proMemory = opts.proOperator || opts.adapter.nativeTools ? (opts.proMemory ?? new ProTaskMemory()) : undefined;
86
94
  }
87
95
  /** Push feedback into both the pending queue and the immutable log. */
@@ -90,15 +98,28 @@ export class AgentLoop {
90
98
  this.feedbackLog.push(item);
91
99
  }
92
100
  async run() {
101
+ const budget = new AbortController();
102
+ this.deadline = Date.now() + (this.opts.timeoutMs ?? AgentLoop.DEFAULT_TIMEOUT_MS);
103
+ this.signal = AbortSignal.any([budget.signal, ...(this.opts.signal ? [this.opts.signal] : [])]);
104
+ const timer = setTimeout(() => budget.abort(this.timeoutReason), Math.max(0, this.deadline - Date.now()));
105
+ timer.unref?.();
106
+ try {
107
+ return await this.runLoop();
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ }
112
+ }
113
+ async runLoop() {
93
114
  const maxSteps = this.opts.maxSteps ?? AgentLoop.DEFAULT_MAX_STEPS;
94
115
  const timeoutMs = this.opts.timeoutMs ?? AgentLoop.DEFAULT_TIMEOUT_MS;
95
- const deadline = Date.now() + timeoutMs;
116
+ const deadline = this.deadline;
96
117
  const usage = { inputTokens: 0, outputTokens: 0 };
97
118
  let step = 0;
98
119
  let lastError;
99
120
  let finalText = "";
100
121
  while (step < maxSteps) {
101
- if (this.opts.signal?.aborted) {
122
+ if (this.signal?.aborted) {
102
123
  return this.finish("cancelled", step, "Task cancelled", usage, lastError);
103
124
  }
104
125
  if (Date.now() > deadline) {
@@ -115,18 +136,18 @@ export class AgentLoop {
115
136
  let obs;
116
137
  try {
117
138
  try {
118
- obs = await this.observe(deadline);
139
+ obs = await abortableRead(() => this.observe(deadline), this.signal);
119
140
  }
120
141
  catch (error) {
121
142
  if (!this.opts.adapter.retryObservation || !/timed out|timeout|ECONNRESET/i.test(errMsg(error)) || Date.now() >= deadline)
122
143
  throw error;
123
144
  const retrySignal = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
124
- await delay(250, undefined, { signal: this.opts.signal ? AbortSignal.any([this.opts.signal, retrySignal]) : retrySignal });
125
- obs = await this.observe(deadline);
145
+ await delay(250, undefined, { signal: this.signal ? AbortSignal.any([this.signal, retrySignal]) : retrySignal });
146
+ obs = await abortableRead(() => this.observe(deadline), this.signal);
126
147
  }
127
148
  }
128
149
  catch (e) {
129
- if (this.opts.signal?.aborted) {
150
+ if (this.signal?.aborted) {
130
151
  return this.finish("cancelled", step, "Task cancelled", usage, lastError);
131
152
  }
132
153
  const err = {
@@ -142,11 +163,15 @@ export class AgentLoop {
142
163
  return this.finish("timeout", step, `agent exceeded ${timeoutMs}ms budget`, usage, lastError);
143
164
  }
144
165
  // Compute a normalised screen-state fingerprint. Used downstream by
145
- // reflectors to detect "no progress" stalls more reliably than
166
+ // execution checks to detect "no progress" stalls more reliably than
146
167
  // coordinate-similarity heuristics. Only meaningful when uiDump is
147
168
  // present; pure-vision flows fall back to the legacy 8px rule.
148
- obs.screenStateHash = computeScreenStateHash(obs.uiDump);
169
+ obs.screenStateHash = computeScreenStateHash(obs.uiDump) ??
170
+ (this.opts.adapter.nativeTools ? await visualStateHash(obs.screenshotB64) : undefined);
171
+ if (this.signal?.aborted)
172
+ return this.finish("cancelled", step, "Task cancelled", usage, lastError);
149
173
  this.proMemory?.observe(obs);
174
+ this.observationIndex = step;
150
175
  await this.fire("onObservation", { index: step, observation: obs });
151
176
  // Stamp the previous step's `screenStateHashAfter` with the current
152
177
  // observation's fingerprint — the MCP act + roundtrip + subsequent
@@ -163,12 +188,12 @@ export class AgentLoop {
163
188
  // uiDump's top text/content-desc nodes (when the effective observation
164
189
  // mode includes XML); otherwise we fall back to the
165
190
  // grounder's rationale once predict() returns. This is what
166
- // downstream reflectors read so they don't need the screenshot.
191
+ // downstream execution checks read so they don't need the screenshot.
167
192
  let observationSummary = summarizeUiDump(obs.uiDump);
168
- this.evidence = this.opts.proOperator ? { uiDump: obs.uiDump?.slice(0, 12000),
193
+ this.evidence = this.opts.proOperator || this.opts.adapter.nativeTools ? { uiDump: obs.uiDump ? compactUiXmlEvidence(obs.uiDump) : undefined,
169
194
  screenshotB64: obs.uiDump ? undefined : obs.screenshotB64 } : undefined;
170
195
  // ── 2. Predict ────────────────────────────────────────────────
171
- // Splice any pending reflector feedback into the prompt so the
196
+ // Splice any pending execution-check feedback into the prompt so the
172
197
  // grounder sees that its previous action was blocked / failed
173
198
  // before it picks the next one. This closes the reflection
174
199
  // feedback loop without touching adapter protocols. The drained
@@ -184,36 +209,44 @@ export class AgentLoop {
184
209
  let prediction;
185
210
  let streamedPredictionText = false;
186
211
  try {
187
- const mcpTools = this.opts.adapter.nativeTools ? await this.opts.mcp.listTools?.() : undefined;
212
+ const mcpTools = this.opts.adapter.nativeTools
213
+ ? await abortableRead(async () => this.opts.mcp.listTools?.(), this.signal) : undefined;
188
214
  if (this.opts.adapter.nativeTools && !mcpTools)
189
215
  throw new Error("Native model tools require an MCP tools/list-capable runtime");
216
+ const queued = this.batch.next(obs, this.history);
190
217
  const modelHistory = this.opts.proOperator
191
218
  ? [...(this.opts.priorHistory ?? []), ...this.history]
192
219
  : this.history;
193
- prediction = await this.opts.adapter.predictAction(obsForPredict, modelHistory, {
220
+ prediction = queued ?? await abortableRead(() => this.opts.adapter.predictAction(obsForPredict, modelHistory, {
194
221
  mcpTools,
195
- signal: this.opts.signal,
222
+ signal: this.signal,
196
223
  contextInstruction: this.opts.proOperator
197
224
  ? renderProOperatorContext(obsForPredict.task, modelHistory) +
225
+ `\n${this.opts.operatorGuidance ?? ""}` +
198
226
  `\n${this.proMemory?.render(obsForPredict.task) ?? ""}` +
199
227
  (obs.extra?.ocrStatus === "unavailable" ? "\nOCR failed; absence of OCR text is not evidence that the screen has no text." : "")
200
228
  : this.opts.adapter.nativeTools ? this.proMemory?.render(obsForPredict.task) : undefined,
201
229
  hooks: {
202
230
  onText: this.opts.hooks?.onText
203
231
  ? async (delta) => {
204
- if (!delta)
232
+ if (!delta || this.signal?.aborted)
205
233
  return;
206
234
  streamedPredictionText = true;
207
235
  await this.fire("onText", delta);
208
236
  }
209
237
  : undefined,
210
- onReasoning: this.opts.hooks?.onReasoning,
238
+ onReasoning: this.opts.hooks?.onReasoning
239
+ ? async (delta) => { if (!this.signal?.aborted)
240
+ await this.opts.hooks?.onReasoning?.(delta); }
241
+ : undefined,
211
242
  },
212
- });
243
+ }), this.signal);
244
+ if (!queued)
245
+ this.batch.enqueue(prediction, obs);
213
246
  this.pendingFeedback = [];
214
247
  }
215
248
  catch (e) {
216
- if (this.opts.signal?.aborted) {
249
+ if (this.signal?.aborted) {
217
250
  return this.finish("cancelled", step, "Task cancelled", usage, lastError);
218
251
  }
219
252
  const fatal = isPermanentModelError(e);
@@ -244,6 +277,11 @@ export class AgentLoop {
244
277
  }
245
278
  continue;
246
279
  }
280
+ if (this.signal?.aborted) {
281
+ prediction.toolResult = { ok: false, executed: false, error: "Cancelled before dispatch" };
282
+ this.recordHistory(step, prediction, observationSummary, "skipped", "Cancelled", obs.screenStateHash, false);
283
+ return this.finish("cancelled", step, "Task cancelled", usage, lastError);
284
+ }
247
285
  usage.inputTokens += prediction.usage.inputTokens;
248
286
  usage.outputTokens += prediction.usage.outputTokens;
249
287
  if (prediction.usage.cachedInputTokens) {
@@ -262,11 +300,18 @@ export class AgentLoop {
262
300
  if (prediction.rationale && !streamedPredictionText) {
263
301
  await this.fire("onText", prediction.rationale);
264
302
  }
303
+ if (this.signal?.aborted) {
304
+ prediction.toolResult = { ok: false, executed: false, error: "Cancelled before dispatch" };
305
+ this.recordHistory(step, prediction, observationSummary, "skipped", "Cancelled", obs.screenStateHash, false);
306
+ return this.finish("cancelled", step, "Task cancelled", usage, lastError);
307
+ }
265
308
  // ── 3. Act ─────────────────────────────────────────────────────
266
309
  const action = prediction.action;
310
+ if (action.payload._proRejected)
311
+ prediction.toolResult = { ok: false, executed: false, error: action.payload._proRejected };
267
312
  // Phase 2.1 — `take_note` is a *local* book-keeping verb. It does
268
313
  // NOT hit MCP, does NOT consume act / pre-sanity budget on the
269
- // grounder side, and is recorded in history so reflectors / replan
314
+ // grounder side, and is recorded in history so execution checks / replan
270
315
  // can see it. Step counter still ticks (we already incremented
271
316
  // above) so a runaway grounder that only emits notes still hits
272
317
  // the heuristic veto.
@@ -275,7 +320,7 @@ export class AgentLoop {
275
320
  const call = proCalls(action)[0];
276
321
  await this.fire("onTool", { name: call.name, args: {}, status: "start" });
277
322
  try {
278
- this.opts.signal?.throwIfAborted();
323
+ this.signal?.throwIfAborted();
279
324
  const local = action.payload?._proLocal;
280
325
  if (local === "search_memory")
281
326
  prediction.toolResult = this.proMemory?.search(String(action.payload?.query ?? "")) ?? [];
@@ -290,14 +335,12 @@ export class AgentLoop {
290
335
  else if (local && local !== "take_note") {
291
336
  if (!this.opts.mcp.callTool)
292
337
  throw new Error("MCP callTool unavailable");
293
- const r = await this.opts.mcp.callTool(call.name, JSON.parse(call.arguments));
338
+ const r = await abortableRead(() => this.opts.mcp.callTool(call.name, JSON.parse(call.arguments), { signal: this.signal }), this.signal);
294
339
  prediction.toolResult = { ok: r.ok, data: r.structured ?? r.raw };
295
340
  }
296
- this.opts.signal?.throwIfAborted();
341
+ this.signal?.throwIfAborted();
297
342
  }
298
343
  catch (e) {
299
- if (this.opts.signal?.aborted)
300
- return this.finish("cancelled", step, "Task cancelled", usage, lastError);
301
344
  prediction.toolResult = { ok: false, error: errMsg(e) };
302
345
  }
303
346
  const failed = prediction.toolResult?.ok === false;
@@ -307,8 +350,10 @@ export class AgentLoop {
307
350
  if (note)
308
351
  this.notes.push(note);
309
352
  }
310
- this.recordHistory(step, prediction, observationSummary, failed ? "act_failed" : "ok", failed ? JSON.stringify(prediction.toolResult) : undefined, obs.screenStateHash);
353
+ this.recordHistory(step, prediction, observationSummary, failed ? "act_failed" : "ok", failed ? JSON.stringify(prediction.toolResult) : undefined, obs.screenStateHash, false);
311
354
  await this.fireStep(step, prediction, false);
355
+ if (this.signal?.aborted)
356
+ return this.finish("cancelled", step, "Task cancelled", usage, lastError);
312
357
  continue;
313
358
  }
314
359
  const note = stringFromPayload(action.payload, "note") ?? prediction.rationale ?? "";
@@ -321,7 +366,7 @@ export class AgentLoop {
321
366
  this.vetoCodeCount = {};
322
367
  // Heuristic veto still applies (4 identical take_note in a row =
323
368
  // stalled grounder), so fall through to the bottom check.
324
- const veto = this.reflector.maybeVeto(this.history, step, maxSteps);
369
+ const veto = this.executionChecks.maybeVeto(this.history, step, maxSteps);
325
370
  if (veto) {
326
371
  const err = {
327
372
  code: "REFLECTION_VETO",
@@ -331,25 +376,13 @@ export class AgentLoop {
331
376
  await this.fire("onError", err);
332
377
  return this.finish("reflection_veto", step, veto, usage, err);
333
378
  }
334
- // Trajectory check still applies — a runaway grounder that only
335
- // emits notes is exactly the "stuck" case we want to bail on.
336
- const trajVeto = await this.maybeRunTrajectoryReflector(step, maxSteps, usage);
337
- if (trajVeto) {
338
- const err = {
339
- code: "REFLECTION_VETO",
340
- message: trajVeto,
341
- fatal: true,
342
- };
343
- await this.fire("onError", err);
344
- return this.finish("reflection_veto", step, trajVeto, usage, err);
345
- }
346
379
  continue;
347
380
  }
348
381
  // Pre-action sanity (Phase 1.3) — block obviously-broken candidates
349
382
  // before dispatch. Terminal verbs are short-circuited below so they
350
383
  // are never blocked.
351
384
  if (!isTerminal(action)) {
352
- const veto = this.reflector.maybeBlockAction(this.history, action);
385
+ const veto = this.executionChecks.maybeBlockAction(this.history, action);
353
386
  if (veto) {
354
387
  const note = `pre-sanity blocked: ${veto.code} — ${veto.reason}`;
355
388
  this.recordHistory(step, prediction, note, "skipped", veto.reason, obs.screenStateHash);
@@ -428,86 +461,91 @@ export class AgentLoop {
428
461
  return this.finish("call_for_user", step, finalText, usage, lastError);
429
462
  }
430
463
  // Phase-1: surfacing the event is enough — wait-for-user is
431
- // implemented by the orchestrator layer (OrchestratorAgent).
464
+ // not available in the Android runtime.
432
465
  return this.finish("call_for_user", step, finalText, usage, lastError);
433
466
  }
434
467
  const toolArgs = actionToToolArgs(action);
468
+ const toolName = proCalls(action)[0]?.name ?? action.type;
435
469
  await this.fire("onTool", {
436
- name: action.type,
470
+ name: toolName,
437
471
  args: toolArgs,
438
472
  status: "start",
439
473
  });
440
474
  const t0 = Date.now();
441
475
  let actResult;
476
+ let actionDispatched = false;
442
477
  try {
443
478
  let dispatchAction = this.scaleAction(action, obs);
444
479
  this.proMemory?.assertCanScroll(dispatchAction);
445
- if (this.opts.proOperator || this.opts.adapter.nativeTools) {
446
- dispatchAction = await checkProPerception(dispatchAction, obs, this.opts.mcp, this.opts.signal);
480
+ if ((this.opts.proOperator || this.opts.adapter.nativeTools) && !this.opts.adapter.checkAction) {
481
+ dispatchAction = await abortableRead(() => checkProPerception(dispatchAction, obs, this.opts.mcp, this.signal), this.signal);
447
482
  }
448
483
  if (this.opts.adapter.checkAction && !["wait", "home", "back", "launch_app"].includes(action.type)) {
449
484
  const signal = AbortSignal.any([AbortSignal.timeout(Math.max(1, deadline - Date.now())),
450
- ...(this.opts.signal ? [this.opts.signal] : [])]);
451
- const fresh = await this.observe(deadline);
452
- const verdict = await this.opts.adapter.checkAction(dispatchAction, fresh, prediction.rationale, signal);
485
+ ...(this.signal ? [this.signal] : [])]);
486
+ const fresh = await abortableRead(() => this.observe(deadline), signal);
487
+ // Grounding and semantic review share one fresh snapshot. Still
488
+ // recheck drift after the model review, immediately before dispatch.
489
+ dispatchAction = await abortableRead(() => checkProPerception(dispatchAction, obs, this.opts.mcp, signal, fresh), signal);
490
+ const intent = stringFromPayload(dispatchAction.payload, "_proIntent") ?? prediction.rationale;
491
+ const verdict = await abortableRead(() => this.opts.adapter.checkAction(dispatchAction, fresh, intent, signal), signal);
453
492
  usage.inputTokens += verdict.usage.inputTokens;
454
493
  usage.outputTokens += verdict.usage.outputTokens;
455
494
  signal.throwIfAborted();
456
495
  if (verdict.decision === "handoff") {
457
496
  const message = `Action not executed. Human confirmation/manual action required: ${verdict.reason}`;
458
497
  this.recordHistory(step, prediction, observationSummary, "skipped", message, obs.screenStateHash);
459
- await this.fire("onTool", { name: action.type, args: toolArgs, status: "end", isError: true,
498
+ await this.fire("onTool", { name: toolName, args: toolArgs, status: "end", isError: true,
460
499
  result: { confirmationRequired: true, executed: false, message } });
461
500
  return this.finish("call_for_user", step, message, usage, lastError);
462
501
  }
463
502
  if (verdict.decision !== "allow")
464
503
  throw new Error(`Action target rejected: ${verdict.reason}`);
465
504
  // Semantic review takes time: reject drift again immediately before dispatch.
466
- await checkProPerception(dispatchAction, fresh, this.opts.mcp, signal);
505
+ await abortableRead(() => checkProPerception(dispatchAction, fresh, this.opts.mcp, signal), signal);
467
506
  }
468
- this.opts.signal?.throwIfAborted();
469
- if (Date.now() >= deadline)
507
+ this.signal?.throwIfAborted();
508
+ if (Date.now() >= deadline) {
509
+ prediction.toolResult = { ok: false, executed: false, error: "Task deadline reached before dispatch" };
510
+ this.recordHistory(step, prediction, observationSummary, "skipped", "Deadline reached", obs.screenStateHash, false);
511
+ await this.fire("onTool", { name: toolName, args: toolArgs, status: "end", isError: true, result: prediction.toolResult });
470
512
  return this.finish("timeout", step, "Task deadline reached before dispatch", usage, { code: "TIMEOUT", message: "Task deadline reached before dispatch", fatal: true });
513
+ }
471
514
  const call = proCalls(dispatchAction)[0];
472
515
  let r;
473
516
  if (call && action.payload?._proSequence) {
474
517
  if (!this.opts.mcp.callTool)
475
518
  throw new Error("MCP callTool unavailable");
476
519
  const points = action.payload._proSequence;
477
- const completed = [];
478
- prediction.toolResult = { ok: false, completed };
479
- for (const point of points) {
480
- this.opts.signal?.throwIfAborted();
481
- if (Date.now() >= deadline)
482
- throw new Error("Task deadline reached during click sequence");
483
- const result = await this.opts.mcp.callTool("tap", point);
484
- if (!result.ok)
485
- throw new Error(`Sequence stopped after ${completed.length} taps: ${JSON.stringify(result.structured ?? result.raw)}`);
486
- completed.push(point);
487
- if (completed.length < points.length)
488
- await delay(50, undefined, { signal: this.opts.signal });
489
- }
490
- prediction.toolResult = { ok: true, completed, message: "Taps dispatched; verify the resulting screen" };
491
- r = { ok: true, action: action.type };
520
+ if (step + points.length - 1 > maxSteps)
521
+ throw new Error("Insufficient action budget for click_sequence");
522
+ step += points.length - 1;
523
+ actionDispatched = true;
524
+ const result = await this.opts.mcp.callTool("click_sequence", { points }, { signal: this.signal });
525
+ prediction.toolResult = { ok: result.ok, data: result.structured ?? result.raw };
526
+ r = { ok: result.ok, action: action.type, error: result.ok ? undefined : { message: JSON.stringify(prediction.toolResult) } };
492
527
  }
493
528
  else if (action.type === "wait") {
494
- await waitForAction(action.payload, this.opts.signal, deadline);
529
+ await waitForAction(action.payload, this.signal, deadline);
495
530
  r = { ok: true, action: action.type };
496
531
  }
497
532
  else if (call && action.payload?._proMcp) {
498
533
  if (!this.opts.mcp.callTool)
499
534
  throw new Error("MCP callTool unavailable");
500
- const result = await this.opts.mcp.callTool(String(action.payload._proMcp), action.payload._proDispatchArgs ?? JSON.parse(call.arguments));
535
+ actionDispatched = true;
536
+ const result = await this.opts.mcp.callTool(String(action.payload._proMcp), action.payload._proDispatchArgs ?? JSON.parse(call.arguments), { signal: this.signal });
501
537
  prediction.toolResult = { ok: result.ok, data: result.structured ?? result.raw };
502
538
  r = { ok: result.ok, action: action.type, error: result.ok ? undefined : { message: JSON.stringify(prediction.toolResult) } };
503
539
  }
504
- else
505
- r = await this.opts.mcp.act(dispatchAction);
540
+ else {
541
+ actionDispatched = true;
542
+ r = await this.opts.mcp.act(dispatchAction, { signal: this.signal });
543
+ }
506
544
  // The MCP `act()` envelope can fail two ways:
507
545
  // 1. throw — network / unreachable / SDK transport error (caught below)
508
546
  // 2. resolve with `ok: false` — server returned `isError: true`
509
547
  // OR (post r3 ActionableError model) `structuredContent.actionable: true`.
510
- // Both paths must follow the act_failed branch so the reflector /
548
+ // Both paths must follow the act_failed branch so the executionChecks /
511
549
  // feedback layer sees the same shape; the only difference is that
512
550
  // actionable errors carry `error.subtype` and we surface that to the
513
551
  // grounder so the LLM can self-correct ("you passed an invalid path"
@@ -532,7 +570,7 @@ export class AgentLoop {
532
570
  fatal: false,
533
571
  };
534
572
  await this.fire("onTool", {
535
- name: action.type,
573
+ name: toolName,
536
574
  args: toolArgs,
537
575
  status: "end",
538
576
  result: errInfo,
@@ -548,17 +586,21 @@ export class AgentLoop {
548
586
  code: isActionable ? `actionable:${subtype ?? "unknown"}` : (subtype ?? "act_failed"),
549
587
  reason: errInfo.message,
550
588
  });
551
- await this.maybeRunActionReflector(prediction, "act_failed", usage);
552
589
  continue;
553
590
  }
554
591
  actResult = r.result ?? r;
555
592
  }
556
593
  catch (e) {
557
- if (this.opts.signal?.aborted)
594
+ if (this.signal?.aborted) {
595
+ prediction.toolResult ??= { ok: false, executed: actionDispatched ? "unknown" : false,
596
+ error: "Cancelled during dispatch; do not assume success or replay without observation." };
597
+ this.recordHistory(step, prediction, observationSummary, "act_failed", "Cancelled", obs.screenStateHash, actionDispatched);
598
+ await this.fire("onTool", { name: toolName, args: toolArgs, status: "end", isError: true, result: prediction.toolResult });
558
599
  return this.finish("cancelled", step, "Task cancelled", usage, lastError);
600
+ }
559
601
  if (action.payload?._proSequence)
560
602
  prediction.toolResult = {
561
- ...prediction.toolResult, ok: false, error: errMsg(e),
603
+ ...prediction.toolResult, ok: false, executed: actionDispatched ? "unknown" : false, error: errMsg(e),
562
604
  };
563
605
  const err = {
564
606
  code: "MCP_ACT_FAILED",
@@ -567,7 +609,7 @@ export class AgentLoop {
567
609
  cause: e,
568
610
  };
569
611
  await this.fire("onTool", {
570
- name: action.type,
612
+ name: toolName,
571
613
  args: toolArgs,
572
614
  status: "end",
573
615
  result: err.message,
@@ -577,7 +619,7 @@ export class AgentLoop {
577
619
  lastError = err;
578
620
  await this.fire("onError", err);
579
621
  // Record the failed step, then keep going so the model can recover.
580
- this.recordHistory(step, prediction, observationSummary, "act_failed", err.message, obs.screenStateHash);
622
+ this.recordHistory(step, prediction, observationSummary, "act_failed", err.message, obs.screenStateHash, actionDispatched);
581
623
  await this.fireStep(step, prediction, false);
582
624
  // Surface the failure to the next predict so the grounder doesn't
583
625
  // assume the action took effect.
@@ -586,14 +628,11 @@ export class AgentLoop {
586
628
  code: "act_failed",
587
629
  reason: err.message,
588
630
  });
589
- // Action reflector is most useful exactly here — fire it before
590
- // the next predict so the grounder sees the verdict in history.
591
- await this.maybeRunActionReflector(prediction, "act_failed", usage);
592
631
  continue;
593
632
  }
594
633
  const durationMs = Date.now() - t0;
595
634
  await this.fire("onTool", {
596
- name: action.type,
635
+ name: toolName,
597
636
  args: toolArgs,
598
637
  status: "end",
599
638
  result: actResult,
@@ -605,10 +644,8 @@ export class AgentLoop {
605
644
  // so a future legitimate retry sequence isn't penalised by stale
606
645
  // counts from an earlier loop arc.
607
646
  this.vetoCodeCount = {};
608
- // ── 4. Reflect (model-based action reflector — Phase 1.4) ──────
609
- await this.maybeRunActionReflector(prediction, "ok", usage);
610
647
  // ── 5. Heuristic veto ─────────────────────────────────────────
611
- const veto = this.reflector.maybeVeto(this.history, step, maxSteps);
648
+ const veto = this.executionChecks.maybeVeto(this.history, step, maxSteps);
612
649
  if (veto) {
613
650
  const err = {
614
651
  code: "REFLECTION_VETO",
@@ -618,18 +655,9 @@ export class AgentLoop {
618
655
  await this.fire("onError", err);
619
656
  return this.finish("reflection_veto", step, veto, usage, err);
620
657
  }
621
- // ── 6. Trajectory reflector (Phase 2.2) ───────────────────────
622
- const trajVeto = await this.maybeRunTrajectoryReflector(step, maxSteps, usage);
623
- if (trajVeto) {
624
- const err = {
625
- code: "REFLECTION_VETO",
626
- message: trajVeto,
627
- fatal: true,
628
- };
629
- await this.fire("onError", err);
630
- return this.finish("reflection_veto", step, trajVeto, usage, err);
631
- }
632
658
  }
659
+ if (this.signal?.aborted)
660
+ return this.finish("cancelled", step, "Task cancelled", usage, lastError);
633
661
  const err = {
634
662
  code: "STEP_BUDGET",
635
663
  message: `hit step budget ${maxSteps}`,
@@ -649,7 +677,6 @@ export class AgentLoop {
649
677
  : resolveObservationMode(requested, this.opts.adapter);
650
678
  const needsVision = mode === "vision" || mode === "hybrid";
651
679
  const needsXml = mode === "xml" || mode === "hybrid";
652
- const ss = needsVision ? await this.opts.mcp.screenshot() : undefined;
653
680
  let uiDump;
654
681
  if (needsXml) {
655
682
  try {
@@ -666,6 +693,12 @@ export class AgentLoop {
666
693
  if (mode === "xml" && !uiDump) {
667
694
  throw new Error("XML observation requested but the device backend returned no UI hierarchy");
668
695
  }
696
+ // UI acquisition can wait through a transition. Capture pixels afterwards,
697
+ // not before that wait, so a successful dump cannot leave a stale image.
698
+ this.signal?.throwIfAborted();
699
+ if (Date.now() >= deadline)
700
+ throw new Error("Observation deadline reached during UI acquisition");
701
+ const ss = needsVision ? await this.opts.mcp.screenshot() : undefined;
669
702
  let screenW = ss?.deviceWidth ?? ss?.width ?? 0;
670
703
  let screenH = ss?.deviceHeight ?? ss?.height ?? 0;
671
704
  if (!ss) {
@@ -674,10 +707,8 @@ export class AgentLoop {
674
707
  screenH = size.h;
675
708
  }
676
709
  // Use the *click coordinate system* (deviceWidth/Height) for scaling —
677
- // not the raw image dimensions. They typically match on Android (`wm
678
- // size` ≡ rendered bytes) but diverge on macOS Retina (downscaled to
679
- // logical points) and iOS (logical vs. physical pixels). Falling back
680
- // to width/height keeps older mcp-server builds working — see
710
+ // not resized image dimensions. Falling back to width/height keeps older
711
+ // Android mcp-server builds working — see
681
712
  // `ScreenshotResult` docstring in `@beeos-ai/device-common`.
682
713
  let ocrText;
683
714
  let ocrStatus;
@@ -686,8 +717,8 @@ export class AgentLoop {
686
717
  if (ocr && ss?.b64) {
687
718
  try {
688
719
  const ocrDeadline = AbortSignal.timeout(Math.min(10_000, Math.max(1, deadline - Date.now())));
689
- const recognized = await ocr.recognize(ss.b64, this.opts.signal
690
- ? AbortSignal.any([this.opts.signal, ocrDeadline]) : ocrDeadline);
720
+ const recognized = await ocr.recognize(ss.b64, this.signal
721
+ ? AbortSignal.any([this.signal, ocrDeadline]) : ocrDeadline);
691
722
  const sx = screenW / (ss.width || screenW);
692
723
  const sy = screenH / (ss.height || screenH);
693
724
  ocrText = recognized.map((item) => ({ ...item,
@@ -696,7 +727,7 @@ export class AgentLoop {
696
727
  ocrStatus = "ok";
697
728
  }
698
729
  catch {
699
- this.opts.signal?.throwIfAborted();
730
+ this.signal?.throwIfAborted();
700
731
  ocrStatus = "unavailable";
701
732
  }
702
733
  }
@@ -707,8 +738,9 @@ export class AgentLoop {
707
738
  screenH,
708
739
  screenshotB64: ss?.b64 ?? "",
709
740
  uiDump,
710
- extra: ss?.currentApp || ocrStatus
741
+ extra: ss?.currentApp || ocrStatus || this.opts.taskContext
711
742
  ? { ...(ss?.currentApp ? { currentApp: ss.currentApp } : {}),
743
+ ...(this.opts.taskContext ? { taskContext: this.opts.taskContext } : {}),
712
744
  ...(ocrStatus ? { ocrStatus, ocrText } : {}), }
713
745
  : undefined,
714
746
  };
@@ -741,122 +773,7 @@ export class AgentLoop {
741
773
  }
742
774
  return scaled;
743
775
  }
744
- /**
745
- * Phase 1.4 — invoke the optional model-based action reflector. Verdicts
746
- * are advisory: a `passed: false` result is appended to the just-pushed
747
- * StepHistory entry's `rationale` (so the next `predict` sees it), and
748
- * the entry's `actionResult` is upgraded from `ok` → `act_failed` so
749
- * downstream code (skill evolver, trajectory reflector) treats it as a
750
- * miss.
751
- *
752
- * The reflector itself is responsible for {@link IActionReflector.shouldReflect}
753
- * gating; this helper just composes the wiring. Cost is rolled into the
754
- * surrounding {@link AgentLoop.run} `usage` aggregator.
755
- */
756
- async maybeRunActionReflector(prediction, actionResult, usage) {
757
- const reflector = this.reflector.actionReflector;
758
- if (!reflector)
759
- return;
760
- if (!reflector.shouldReflect(this.history, prediction, actionResult))
761
- return;
762
- const last = this.history[this.history.length - 1];
763
- if (!last)
764
- return;
765
- try {
766
- const verdict = await reflector.reflect({
767
- goal: this.opts.task,
768
- history: this.history,
769
- prediction,
770
- observationSummary: last.observationSummary ?? "",
771
- }, this.opts.signal);
772
- // Roll usage in — even passing reflections cost tokens.
773
- usage.inputTokens += verdict.usage.inputTokens;
774
- usage.outputTokens += verdict.usage.outputTokens;
775
- if (verdict.usage.costUsd) {
776
- usage.costUsd = (usage.costUsd ?? 0) + verdict.usage.costUsd;
777
- }
778
- if (!verdict.passed) {
779
- // Splice the verdict into the just-recorded step so the grounder's
780
- // next predict reads it. Upgrade outcome to act_failed so reflectors
781
- // / evolver downstream treat the step as a miss even if MCP succeeded.
782
- last.rationale =
783
- (last.rationale ? last.rationale + "\n" : "") +
784
- `[action_reflector] ${verdict.reason}`;
785
- last.actionResult = "act_failed";
786
- last.actErrorMessage =
787
- last.actErrorMessage ?? `action reflector flagged: ${verdict.reason}`;
788
- }
789
- }
790
- catch {
791
- // Reflector failures must NOT abort the loop. The reflector
792
- // implementation is expected to swallow its own errors and return a
793
- // `passed: true` verdict — but if it doesn't, the catch-all here is
794
- // the final safety net.
795
- }
796
- }
797
- /**
798
- * Phase 2.2 — periodic global progress assessment. The reflector decides
799
- * its own gating (default: every 5 steps); we just compose the wiring.
800
- *
801
- * Returns a non-empty veto string only on `stuck`. `off_track` injects an
802
- * advisory line into the most recent history entry's rationale so the
803
- * grounder sees it next predict, but the loop keeps running. The verdict
804
- * itself is stashed on `this.trajectoryVerdict` so the PlanExecutor can
805
- * thread it into `PlanContext.trajectoryVerdict` on replan.
806
- */
807
- async maybeRunTrajectoryReflector(step, maxSteps, usage) {
808
- const reflector = this.reflector.trajectoryReflector;
809
- if (!reflector)
810
- return undefined;
811
- if (!reflector.shouldReflect(step, this.history))
812
- return undefined;
813
- const last = this.history[this.history.length - 1];
814
- const observationSummary = last?.observationSummary ?? "";
815
- try {
816
- const verdict = await reflector.reflect({
817
- goal: this.opts.task,
818
- step,
819
- maxSteps,
820
- history: this.history,
821
- notes: this.notes,
822
- observationSummary,
823
- }, this.opts.signal);
824
- usage.inputTokens += verdict.usage.inputTokens;
825
- usage.outputTokens += verdict.usage.outputTokens;
826
- if (verdict.usage.costUsd) {
827
- usage.costUsd = (usage.costUsd ?? 0) + verdict.usage.costUsd;
828
- }
829
- if (verdict.verdict === "on_track")
830
- return undefined;
831
- // off_track / stuck — annotate history so grounder sees it.
832
- const tag = `[trajectory_reflector:${verdict.verdict}] ${verdict.reason}${verdict.suggestion ? ` (suggestion: ${verdict.suggestion})` : ""}`;
833
- if (last) {
834
- last.rationale = (last.rationale ? last.rationale + "\n" : "") + tag;
835
- }
836
- this.trajectoryVerdict = { verdict: verdict.verdict, reason: verdict.reason };
837
- // Push into the feedback queue so the next predict (or the planner
838
- // on replan) sees the trajectory verdict explicitly. For `off_track`
839
- // this is the primary signal because the loop keeps going.
840
- this.pushFeedback({
841
- source: "trajectory_reflector",
842
- code: verdict.verdict,
843
- reason: verdict.suggestion
844
- ? `${verdict.reason} (suggestion: ${verdict.suggestion})`
845
- : verdict.reason,
846
- });
847
- if (verdict.verdict === "stuck") {
848
- return `trajectory stuck: ${verdict.reason}`;
849
- }
850
- // off_track is non-fatal — keep going, planner will see the verdict
851
- // on the *next* replan if the loop later bails for another reason.
852
- return undefined;
853
- }
854
- catch {
855
- // Trajectory reflector failures must not abort the loop.
856
- return undefined;
857
- }
858
- }
859
- recordHistory(step, p, summary, actionResult, actErrorMessage, screenStateHashBefore) {
776
+ recordHistory(step, p, summary, actionResult, actErrorMessage, screenStateHashBefore, actionDispatched) {
860
777
  this.proMemory?.record(p.action, actionResult ?? "unknown", actErrorMessage ?? summary);
861
778
  this.history.push({
862
779
  stepIdx: step,
@@ -864,12 +781,19 @@ export class AgentLoop {
864
781
  rationale: p.rationale,
865
782
  observationSummary: summary,
866
783
  actionResult,
784
+ actionDispatched,
867
785
  confidence: p.confidence,
868
786
  actErrorMessage,
869
787
  screenStateHashBefore,
870
788
  toolResult: p.toolResult,
871
789
  evidence: this.evidence,
872
790
  });
791
+ if (this.opts.adapter.nativeTools && !this.opts.proOperator) {
792
+ const images = this.history.filter((entry) => entry.evidence?.screenshotB64);
793
+ for (const entry of images.slice(0, -3)) {
794
+ entry.evidence = { ...entry.evidence, screenshotB64: undefined };
795
+ }
796
+ }
873
797
  }
874
798
  /**
875
799
  * Backfill `screenStateHashAfter` on the just-pushed terminal history
@@ -895,6 +819,7 @@ export class AgentLoop {
895
819
  try {
896
820
  await this.opts.hooks.onStep({
897
821
  index: step,
822
+ observationIndex: this.observationIndex ?? step,
898
823
  rationale: prediction.rationale,
899
824
  action: prediction.action,
900
825
  prediction,
@@ -917,6 +842,15 @@ export class AgentLoop {
917
842
  }
918
843
  }
919
844
  finish(terminator, steps, finalText, usage, lastError) {
845
+ // Cancellation wins over a late terminal prediction or the final step budget.
846
+ if (this.signal?.aborted || Date.now() >= this.deadline) {
847
+ const timedOut = this.signal?.aborted ? this.signal.reason === this.timeoutReason : true;
848
+ terminator = timedOut ? "timeout" : "cancelled";
849
+ finalText = timedOut ? "Task deadline reached" : "Task cancelled";
850
+ if (timedOut)
851
+ lastError = { code: "TIMEOUT", message: finalText, fatal: true };
852
+ }
853
+ this.batch.stop(this.history, `Task ended with ${terminator}; call not executed.`);
920
854
  return {
921
855
  terminator,
922
856
  steps,
@@ -926,7 +860,6 @@ export class AgentLoop {
926
860
  usage,
927
861
  lastError,
928
862
  notes: [...this.notes],
929
- trajectoryVerdict: this.trajectoryVerdict,
930
863
  feedbackLog: [...this.feedbackLog],
931
864
  };
932
865
  }
@@ -1083,9 +1016,9 @@ const NOISY_UI_DUMP_ATTR_RE = new RegExp(`\\b(?:${NOISY_UI_DUMP_ATTRS.join("|")}
1083
1016
  * that look identical to a human should produce the same hash.
1084
1017
  *
1085
1018
  * Returns `undefined` when no `uiDump` is available — pure-vision
1086
- * adapters (Open-AutoGLM, UI-TARS, Qwen-VL) skip the dump round-trip
1019
+ * adapters (AutoGLM) skip the dump round-trip
1087
1020
  * and fall back to the legacy 8px coordinate-similarity heuristic in
1088
- * `HierarchicalReflector.maybeBlockAction`.
1021
+ * `ExecutionChecks.maybeBlockAction`.
1089
1022
  */
1090
1023
  export function computeScreenStateHash(uiDump) {
1091
1024
  if (!uiDump)