@beeos-ai/device-agent 0.15.0 → 0.16.1
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.
- package/README.md +35 -0
- package/dist/adapters/agent-gateway-client.d.ts +6 -68
- package/dist/adapters/agent-gateway-client.js +7 -67
- package/dist/adapters/agent-gateway-client.js.map +1 -1
- package/dist/adapters/device-compat.d.ts +1 -2
- package/dist/adapters/device-compat.js +2 -52
- package/dist/adapters/device-compat.js.map +1 -1
- package/dist/agent-runtime/action-guard.js +92 -17
- package/dist/agent-runtime/action-guard.js.map +1 -1
- package/dist/agent-runtime/adapters/base.d.ts +13 -9
- package/dist/agent-runtime/adapters/base.js.map +1 -1
- package/dist/agent-runtime/adapters/general-flash.d.ts +3 -3
- package/dist/agent-runtime/adapters/general-flash.js +2 -2
- package/dist/agent-runtime/adapters/general-flash.js.map +1 -1
- package/dist/agent-runtime/adapters/native-tools.d.ts +56 -0
- package/dist/agent-runtime/adapters/native-tools.js +416 -0
- package/dist/agent-runtime/adapters/native-tools.js.map +1 -0
- package/dist/agent-runtime/adapters/openai.d.ts +1 -2
- package/dist/agent-runtime/adapters/openai.js +5 -5
- package/dist/agent-runtime/adapters/openai.js.map +1 -1
- package/dist/agent-runtime/adapters/pro.d.ts +7 -37
- package/dist/agent-runtime/adapters/pro.js +7 -249
- package/dist/agent-runtime/adapters/pro.js.map +1 -1
- package/dist/agent-runtime/advanced/app-card.js +9 -2
- package/dist/agent-runtime/advanced/app-card.js.map +1 -1
- package/dist/agent-runtime/advanced/subgoal-verifier.d.ts +1 -0
- package/dist/agent-runtime/advanced/subgoal-verifier.js +30 -36
- package/dist/agent-runtime/advanced/subgoal-verifier.js.map +1 -1
- package/dist/agent-runtime/advanced/task-state.js +4 -9
- package/dist/agent-runtime/advanced/task-state.js.map +1 -1
- package/dist/agent-runtime/agent-runner.d.ts +2 -11
- package/dist/agent-runtime/agent-runner.js +69 -34
- package/dist/agent-runtime/agent-runner.js.map +1 -1
- package/dist/agent-runtime/core.d.ts +24 -46
- package/dist/agent-runtime/core.js +161 -228
- package/dist/agent-runtime/core.js.map +1 -1
- package/dist/agent-runtime/execution-checks.d.ts +40 -0
- package/dist/agent-runtime/execution-checks.js +133 -0
- package/dist/agent-runtime/execution-checks.js.map +1 -0
- package/dist/agent-runtime/execution-mode.d.ts +1 -8
- package/dist/agent-runtime/execution-mode.js +10 -30
- package/dist/agent-runtime/execution-mode.js.map +1 -1
- package/dist/agent-runtime/feedback.d.ts +4 -28
- package/dist/agent-runtime/feedback.js +4 -26
- package/dist/agent-runtime/feedback.js.map +1 -1
- package/dist/agent-runtime/form-prerequisites.d.ts +3 -0
- package/dist/agent-runtime/form-prerequisites.js +52 -0
- package/dist/agent-runtime/form-prerequisites.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +2 -8
- package/dist/agent-runtime/index.js +2 -8
- package/dist/agent-runtime/index.js.map +1 -1
- package/dist/agent-runtime/{modes/orchestrator → knowledge}/app-guide.d.ts +1 -1
- package/dist/agent-runtime/{modes/orchestrator → knowledge}/app-guide.js +1 -1
- package/dist/agent-runtime/knowledge/app-guide.js.map +1 -0
- package/dist/agent-runtime/models/base.d.ts +3 -1
- package/dist/agent-runtime/models/client.js +30 -9
- package/dist/agent-runtime/models/client.js.map +1 -1
- package/dist/agent-runtime/models/reasoning.d.ts +10 -0
- package/dist/agent-runtime/models/reasoning.js +29 -0
- package/dist/agent-runtime/models/reasoning.js.map +1 -0
- package/dist/agent-runtime/models/runtime-registry.d.ts +2 -1
- package/dist/agent-runtime/models/runtime-registry.js +4 -2
- package/dist/agent-runtime/models/runtime-registry.js.map +1 -1
- package/dist/agent-runtime/models/vlm/autoglm.js +6 -2
- package/dist/agent-runtime/models/vlm/autoglm.js.map +1 -1
- package/dist/agent-runtime/models/vlm/base.js +1 -12
- package/dist/agent-runtime/models/vlm/base.js.map +1 -1
- package/dist/agent-runtime/models/vlm/index.d.ts +0 -4
- package/dist/agent-runtime/models/vlm/index.js +0 -4
- package/dist/agent-runtime/models/vlm/index.js.map +1 -1
- package/dist/agent-runtime/modes/hybrid/index.d.ts +2 -12
- package/dist/agent-runtime/modes/hybrid/index.js +10 -27
- package/dist/agent-runtime/modes/hybrid/index.js.map +1 -1
- package/dist/agent-runtime/modes/hybrid/planner/base.d.ts +4 -44
- package/dist/agent-runtime/modes/hybrid/planner/base.js +3 -21
- package/dist/agent-runtime/modes/hybrid/planner/base.js.map +1 -1
- package/dist/agent-runtime/modes/hybrid/planner/llm-planner.d.ts +1 -1
- package/dist/agent-runtime/modes/hybrid/planner/llm-planner.js +9 -6
- package/dist/agent-runtime/modes/hybrid/planner/llm-planner.js.map +1 -1
- package/dist/agent-runtime/modes/hybrid/planner/prompts.d.ts +1 -17
- package/dist/agent-runtime/modes/hybrid/planner/prompts.js +65 -160
- package/dist/agent-runtime/modes/hybrid/planner/prompts.js.map +1 -1
- package/dist/agent-runtime/plan-executor.d.ts +13 -14
- package/dist/agent-runtime/plan-executor.js +236 -122
- package/dist/agent-runtime/plan-executor.js.map +1 -1
- package/dist/agent-runtime/pro-memory.js +1 -1
- package/dist/agent-runtime/pro-memory.js.map +1 -1
- package/dist/agent-runtime/pro-operator.js +8 -2
- package/dist/agent-runtime/pro-operator.js.map +1 -1
- package/dist/agent-runtime/pro-perception.d.ts +1 -0
- package/dist/agent-runtime/pro-perception.js +4 -2
- package/dist/agent-runtime/pro-perception.js.map +1 -1
- package/dist/agent-runtime/pro-target-check.d.ts +2 -2
- package/dist/agent-runtime/pro-target-check.js +102 -17
- package/dist/agent-runtime/pro-target-check.js.map +1 -1
- package/dist/agent-runtime/prompts/operator.d.ts +2 -0
- package/dist/agent-runtime/prompts/operator.js +69 -0
- package/dist/agent-runtime/prompts/operator.js.map +1 -0
- package/dist/agent-runtime/prompts/verification.d.ts +2 -0
- package/dist/agent-runtime/prompts/verification.js +52 -0
- package/dist/agent-runtime/prompts/verification.js.map +1 -0
- package/dist/agent-runtime/skills/replay.d.ts +4 -14
- package/dist/agent-runtime/skills/replay.js +46 -27
- package/dist/agent-runtime/skills/replay.js.map +1 -1
- package/dist/agent-runtime/tool-batch.d.ts +10 -0
- package/dist/agent-runtime/tool-batch.js +51 -0
- package/dist/agent-runtime/tool-batch.js.map +1 -0
- package/dist/agent-runtime/visual-state.d.ts +2 -0
- package/dist/agent-runtime/visual-state.js +24 -0
- package/dist/agent-runtime/visual-state.js.map +1 -0
- package/dist/agent-runtime/vlm.d.ts +1 -1
- package/dist/agent-runtime/vlm.js +1 -1
- package/dist/api.js +3 -1
- package/dist/api.js.map +1 -1
- package/dist/app/agent-host.js +6 -2
- package/dist/app/agent-host.js.map +1 -1
- package/dist/app/multi-device-host.js +4 -5
- package/dist/app/multi-device-host.js.map +1 -1
- package/dist/app/session-manager.d.ts +3 -45
- package/dist/app/session-manager.js +8 -9
- package/dist/app/session-manager.js.map +1 -1
- package/dist/bootstrap.js +25 -84
- package/dist/bootstrap.js.map +1 -1
- package/dist/config/loader.js +12 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +7 -2
- package/dist/config/schema.js +8 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/core/abortable-read.d.ts +1 -0
- package/dist/core/abortable-read.js +24 -0
- package/dist/core/abortable-read.js.map +1 -0
- package/dist/device/ui-xml.d.ts +4 -0
- package/dist/device/ui-xml.js +35 -13
- package/dist/device/ui-xml.js.map +1 -1
- package/dist/fleet/adapters/process/node-process-manager.d.ts +2 -1
- package/dist/fleet/adapters/process/node-process-manager.js +11 -19
- package/dist/fleet/adapters/process/node-process-manager.js.map +1 -1
- package/dist/inbound/acp/handlers/models.js +11 -8
- package/dist/inbound/acp/handlers/models.js.map +1 -1
- package/dist/inbound/acp/handlers/session.d.ts +1 -4
- package/dist/inbound/acp/handlers/session.js +3 -6
- package/dist/inbound/acp/handlers/session.js.map +1 -1
- package/dist/inbound/bma/agent.d.ts +47 -0
- package/dist/inbound/bma/agent.js +130 -0
- package/dist/inbound/bma/agent.js.map +1 -0
- package/dist/inbound/bma/command.d.ts +15 -0
- package/dist/inbound/bma/command.js +98 -0
- package/dist/inbound/bma/command.js.map +1 -0
- package/dist/inbound/bma/model-proxy.d.ts +22 -0
- package/dist/inbound/bma/model-proxy.js +130 -0
- package/dist/inbound/bma/model-proxy.js.map +1 -0
- package/dist/inbound/cli/main.js +12 -2
- package/dist/inbound/cli/main.js.map +1 -1
- package/dist/inbound/cli/run-artifacts.js +16 -7
- package/dist/inbound/cli/run-artifacts.js.map +1 -1
- package/dist/inbound/cli/run-command.d.ts +7 -0
- package/dist/inbound/cli/run-command.js +37 -29
- package/dist/inbound/cli/run-command.js.map +1 -1
- package/dist/index.d.ts +1 -10
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp-client/autospawn.d.ts +7 -9
- package/dist/mcp-client/autospawn.js +16 -12
- package/dist/mcp-client/autospawn.js.map +1 -1
- package/dist/mcp-client/client.d.ts +6 -3
- package/dist/mcp-client/client.js +31 -13
- package/dist/mcp-client/client.js.map +1 -1
- package/dist/mcp-client/runtime.d.ts +5 -2
- package/dist/plugins/agent-sync/index.d.ts +32 -17
- package/dist/plugins/agent-sync/index.js +223 -232
- package/dist/plugins/agent-sync/index.js.map +1 -1
- package/dist/plugins/message-bus/inbound.d.ts +0 -32
- package/dist/plugins/message-bus/inbound.js +3 -0
- package/dist/plugins/message-bus/inbound.js.map +1 -1
- package/dist/plugins/message-bus/ms-channel-notifier.d.ts +1 -0
- package/dist/plugins/message-bus/ms-channel-notifier.js +16 -8
- package/dist/plugins/message-bus/ms-channel-notifier.js.map +1 -1
- package/dist/plugins/native-chat/deadline.d.ts +2 -0
- package/dist/plugins/native-chat/deadline.js +16 -0
- package/dist/plugins/native-chat/deadline.js.map +1 -0
- package/dist/plugins/native-chat/index.d.ts +46 -0
- package/dist/plugins/native-chat/index.js +331 -0
- package/dist/plugins/native-chat/index.js.map +1 -0
- package/dist/plugins/native-chat/reply-stream.d.ts +50 -0
- package/dist/plugins/native-chat/reply-stream.js +81 -0
- package/dist/plugins/native-chat/reply-stream.js.map +1 -0
- package/dist/plugins/native-chat/session-notifier.d.ts +16 -0
- package/dist/plugins/native-chat/session-notifier.js +130 -0
- package/dist/plugins/native-chat/session-notifier.js.map +1 -0
- package/dist/ports/agent-core.d.ts +2 -0
- package/dist/ports/agent-host.d.ts +2 -0
- package/dist/ports/agent-host.js +0 -21
- package/dist/ports/agent-host.js.map +1 -1
- package/dist/ports/device.d.ts +0 -1
- package/dist/ports/device.js +0 -1
- package/dist/ports/device.js.map +1 -1
- package/dist/runtime-authority/device-prompt.gen.d.ts +54 -0
- package/dist/runtime-authority/device-prompt.gen.js +19 -0
- package/dist/runtime-authority/device-prompt.gen.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -5
- package/dist/adapters/device-desktop.d.ts +0 -49
- package/dist/adapters/device-desktop.js +0 -132
- package/dist/adapters/device-desktop.js.map +0 -1
- package/dist/agent-runtime/factory.d.ts +0 -24
- package/dist/agent-runtime/factory.js +0 -25
- package/dist/agent-runtime/factory.js.map +0 -1
- package/dist/agent-runtime/models/vlm/guiowl.d.ts +0 -1
- package/dist/agent-runtime/models/vlm/guiowl.js +0 -249
- package/dist/agent-runtime/models/vlm/guiowl.js.map +0 -1
- package/dist/agent-runtime/models/vlm/maiui.d.ts +0 -1
- package/dist/agent-runtime/models/vlm/maiui.js +0 -142
- package/dist/agent-runtime/models/vlm/maiui.js.map +0 -1
- package/dist/agent-runtime/models/vlm/qwen38.d.ts +0 -1
- package/dist/agent-runtime/models/vlm/qwen38.js +0 -241
- package/dist/agent-runtime/models/vlm/qwen38.js.map +0 -1
- package/dist/agent-runtime/models/vlm/qwenvl.d.ts +0 -1
- package/dist/agent-runtime/models/vlm/qwenvl.js +0 -202
- package/dist/agent-runtime/models/vlm/qwenvl.js.map +0 -1
- package/dist/agent-runtime/modes/orchestrator/app-guide.js.map +0 -1
- package/dist/agent-runtime/modes/orchestrator/index.d.ts +0 -41
- package/dist/agent-runtime/modes/orchestrator/index.js +0 -218
- package/dist/agent-runtime/modes/orchestrator/index.js.map +0 -1
- package/dist/agent-runtime/modes/orchestrator/prompt.d.ts +0 -7
- package/dist/agent-runtime/modes/orchestrator/prompt.js +0 -53
- package/dist/agent-runtime/modes/orchestrator/prompt.js.map +0 -1
- package/dist/agent-runtime/reflection.d.ts +0 -100
- package/dist/agent-runtime/reflection.js +0 -241
- package/dist/agent-runtime/reflection.js.map +0 -1
- package/dist/agent-runtime/reflectors/action-reflector.d.ts +0 -97
- package/dist/agent-runtime/reflectors/action-reflector.js +0 -234
- package/dist/agent-runtime/reflectors/action-reflector.js.map +0 -1
- package/dist/agent-runtime/reflectors/policy.d.ts +0 -56
- package/dist/agent-runtime/reflectors/policy.js +0 -111
- package/dist/agent-runtime/reflectors/policy.js.map +0 -1
- package/dist/agent-runtime/reflectors/trajectory-reflector.d.ts +0 -93
- package/dist/agent-runtime/reflectors/trajectory-reflector.js +0 -224
- package/dist/agent-runtime/reflectors/trajectory-reflector.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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:
|
|
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.
|
|
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.
|
|
451
|
-
const fresh = await this.observe(deadline);
|
|
452
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
678
|
-
//
|
|
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.
|
|
690
|
-
? AbortSignal.any([this.
|
|
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.
|
|
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 (
|
|
1019
|
+
* adapters (AutoGLM) skip the dump round-trip
|
|
1087
1020
|
* and fall back to the legacy 8px coordinate-similarity heuristic in
|
|
1088
|
-
* `
|
|
1021
|
+
* `ExecutionChecks.maybeBlockAction`.
|
|
1089
1022
|
*/
|
|
1090
1023
|
export function computeScreenStateHash(uiDump) {
|
|
1091
1024
|
if (!uiDump)
|