@e2edev/e2e 0.11.0 → 0.13.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.
- package/README.md +7 -7
- package/dist/agent/act.d.ts +43 -9
- package/dist/agent/act.d.ts.map +1 -1
- package/dist/agent/act.js +126 -734
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/action-dispatcher.d.ts +98 -0
- package/dist/agent/action-dispatcher.d.ts.map +1 -0
- package/dist/agent/action-dispatcher.js +319 -0
- package/dist/agent/action-dispatcher.js.map +1 -0
- package/dist/agent/actions.d.ts +16 -1
- package/dist/agent/actions.d.ts.map +1 -1
- package/dist/agent/actions.js +5 -1
- package/dist/agent/actions.js.map +1 -1
- package/dist/agent/default-agent.d.ts +17 -8
- package/dist/agent/default-agent.d.ts.map +1 -1
- package/dist/agent/default-agent.js +65 -43
- package/dist/agent/default-agent.js.map +1 -1
- package/dist/agent/executor.d.ts +30 -2
- package/dist/agent/executor.d.ts.map +1 -1
- package/dist/agent/executor.js.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/interactive-step.d.ts +38 -0
- package/dist/agent/interactive-step.d.ts.map +1 -0
- package/dist/agent/interactive-step.js +131 -0
- package/dist/agent/interactive-step.js.map +1 -0
- package/dist/agent/invocation.d.ts +4 -9
- package/dist/agent/invocation.d.ts.map +1 -1
- package/dist/agent/invocation.js +1 -6
- package/dist/agent/invocation.js.map +1 -1
- package/dist/agent/model/sdk.d.ts +1 -1
- package/dist/agent/model/sdk.js +1 -1
- package/dist/agent/model/tool-choice.d.ts +21 -0
- package/dist/agent/model/tool-choice.d.ts.map +1 -0
- package/dist/agent/model/tool-choice.js +49 -0
- package/dist/agent/model/tool-choice.js.map +1 -0
- package/dist/agent/observation-feed.d.ts +123 -0
- package/dist/agent/observation-feed.d.ts.map +1 -0
- package/dist/agent/observation-feed.js +281 -0
- package/dist/agent/observation-feed.js.map +1 -0
- package/dist/agent/observation.d.ts +14 -1
- package/dist/agent/observation.d.ts.map +1 -1
- package/dist/agent/observation.js +58 -2
- package/dist/agent/observation.js.map +1 -1
- package/dist/agent/operation-queue.d.ts +21 -0
- package/dist/agent/operation-queue.d.ts.map +1 -0
- package/dist/agent/operation-queue.js +27 -0
- package/dist/agent/operation-queue.js.map +1 -0
- package/dist/agent/pixels.d.ts +21 -0
- package/dist/agent/pixels.d.ts.map +1 -0
- package/dist/agent/pixels.js +85 -0
- package/dist/agent/pixels.js.map +1 -0
- package/dist/agent/point-tap.d.ts +52 -0
- package/dist/agent/point-tap.d.ts.map +1 -0
- package/dist/agent/point-tap.js +88 -0
- package/dist/agent/point-tap.js.map +1 -0
- package/dist/agent/primitives.d.ts +7 -1
- package/dist/agent/primitives.d.ts.map +1 -1
- package/dist/agent/primitives.js +96 -25
- package/dist/agent/primitives.js.map +1 -1
- package/dist/agent/replay.d.ts +8 -3
- package/dist/agent/replay.d.ts.map +1 -1
- package/dist/agent/replay.js +44 -15
- package/dist/agent/replay.js.map +1 -1
- package/dist/agent/screen-update.d.ts +50 -18
- package/dist/agent/screen-update.d.ts.map +1 -1
- package/dist/agent/screen-update.js +76 -94
- package/dist/agent/screen-update.js.map +1 -1
- package/dist/agent/step-accounting.d.ts +95 -0
- package/dist/agent/step-accounting.d.ts.map +1 -0
- package/dist/agent/step-accounting.js +201 -0
- package/dist/agent/step-accounting.js.map +1 -0
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +2 -2
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts +4 -4
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +109 -25
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/agent/transcript-compaction.d.ts +38 -0
- package/dist/agent/transcript-compaction.d.ts.map +1 -0
- package/dist/agent/transcript-compaction.js +165 -0
- package/dist/agent/transcript-compaction.js.map +1 -0
- package/dist/cache/recorder.d.ts.map +1 -1
- package/dist/cache/recorder.js +20 -0
- package/dist/cache/recorder.js.map +1 -1
- package/dist/cache/trace.d.ts +28 -1
- package/dist/cache/trace.d.ts.map +1 -1
- package/dist/cache/trace.js +44 -0
- package/dist/cache/trace.js.map +1 -1
- package/dist/cli/docs-url.d.ts +1 -1
- package/dist/cli/docs-url.d.ts.map +1 -1
- package/dist/cli/docs-url.js +1 -1
- package/dist/cli/docs-url.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +71 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/engine-versions.json +1 -1
- package/dist/cli/init/engines.d.ts +26 -16
- package/dist/cli/init/engines.d.ts.map +1 -1
- package/dist/cli/init/engines.js +33 -24
- package/dist/cli/init/engines.js.map +1 -1
- package/dist/cli/init/gateways.js +3 -3
- package/dist/cli/init/mcp-config.d.ts +31 -0
- package/dist/cli/init/mcp-config.d.ts.map +1 -0
- package/dist/cli/init/mcp-config.js +72 -0
- package/dist/cli/init/mcp-config.js.map +1 -0
- package/dist/cli/init/scaffold.d.ts +1 -1
- package/dist/cli/init/scaffold.js +2 -2
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +65 -29
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp.d.ts +15 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +63 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/collect/collect.d.ts +8 -3
- package/dist/collect/collect.d.ts.map +1 -1
- package/dist/collect/collect.js +29 -5
- package/dist/collect/collect.js.map +1 -1
- package/dist/collect/registry.d.ts +20 -5
- package/dist/collect/registry.d.ts.map +1 -1
- package/dist/collect/registry.js +162 -56
- package/dist/collect/registry.js.map +1 -1
- package/dist/collect/select.d.ts +9 -2
- package/dist/collect/select.d.ts.map +1 -1
- package/dist/collect/select.js +60 -12
- package/dist/collect/select.js.map +1 -1
- package/dist/config/agent.d.ts +1 -7
- package/dist/config/agent.d.ts.map +1 -1
- package/dist/config/agent.js +5 -8
- package/dist/config/agent.js.map +1 -1
- package/dist/config/app.d.ts +24 -2
- package/dist/config/app.d.ts.map +1 -1
- package/dist/config/app.js +82 -15
- package/dist/config/app.js.map +1 -1
- package/dist/config/resolve.d.ts +23 -6
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +34 -14
- package/dist/config/resolve.js.map +1 -1
- package/dist/credentials.d.ts +6 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +9 -0
- package/dist/credentials.js.map +1 -1
- package/dist/engine/contract.d.ts +9 -2
- package/dist/engine/contract.d.ts.map +1 -1
- package/dist/engine/contract.js.map +1 -1
- package/dist/engine/index.d.ts +18 -4
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +8 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +3 -0
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/surface.d.ts +6 -2
- package/dist/engine/surface.d.ts.map +1 -1
- package/dist/engine/surface.js.map +1 -1
- package/dist/expect/attempt.d.ts +15 -0
- package/dist/expect/attempt.d.ts.map +1 -0
- package/dist/expect/attempt.js +33 -0
- package/dist/expect/attempt.js.map +1 -0
- package/dist/expect/index.d.ts +7 -5
- package/dist/expect/index.d.ts.map +1 -1
- package/dist/expect/index.js +8 -1
- package/dist/expect/index.js.map +1 -1
- package/dist/expect/poll.d.ts +5 -0
- package/dist/expect/poll.d.ts.map +1 -0
- package/dist/expect/poll.js +118 -0
- package/dist/expect/poll.js.map +1 -0
- package/dist/expect/values.d.ts.map +1 -1
- package/dist/expect/values.js +6 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/explore/body.d.ts.map +1 -1
- package/dist/explore/body.js +2 -0
- package/dist/explore/body.js.map +1 -1
- package/dist/explore/executor.js +16 -11
- package/dist/explore/executor.js.map +1 -1
- package/dist/explore/index.d.ts +3 -2
- package/dist/explore/index.d.ts.map +1 -1
- package/dist/explore/index.js +11 -9
- package/dist/explore/index.js.map +1 -1
- package/dist/explore/plan.d.ts.map +1 -1
- package/dist/explore/plan.js +16 -5
- package/dist/explore/plan.js.map +1 -1
- package/dist/explore/progress.d.ts +43 -0
- package/dist/explore/progress.d.ts.map +1 -0
- package/dist/explore/progress.js +9 -0
- package/dist/explore/progress.js.map +1 -0
- package/dist/explore/state.d.ts +13 -12
- package/dist/explore/state.d.ts.map +1 -1
- package/dist/explore/state.js +28 -11
- package/dist/explore/state.js.map +1 -1
- package/dist/internal/ai-trace.d.ts +2 -0
- package/dist/internal/ai-trace.d.ts.map +1 -1
- package/dist/internal/ai-trace.js +3 -0
- package/dist/internal/ai-trace.js.map +1 -1
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.d.ts.map +1 -1
- package/dist/internal/brands.js +2 -0
- package/dist/internal/brands.js.map +1 -1
- package/dist/internal/geometry.d.ts +13 -0
- package/dist/internal/geometry.d.ts.map +1 -0
- package/dist/internal/geometry.js +16 -0
- package/dist/internal/geometry.js.map +1 -0
- package/dist/internal/ids.d.ts +2 -2
- package/dist/internal/ids.d.ts.map +1 -1
- package/dist/internal/ids.js +3 -3
- package/dist/internal/ids.js.map +1 -1
- package/dist/internal/package-manager.d.ts +4 -0
- package/dist/internal/package-manager.d.ts.map +1 -1
- package/dist/internal/package-manager.js +26 -0
- package/dist/internal/package-manager.js.map +1 -1
- package/dist/internal/urls.d.ts +6 -0
- package/dist/internal/urls.d.ts.map +1 -1
- package/dist/internal/urls.js +28 -0
- package/dist/internal/urls.js.map +1 -1
- package/dist/mcp/catalog.d.ts +51 -0
- package/dist/mcp/catalog.d.ts.map +1 -0
- package/dist/mcp/catalog.js +141 -0
- package/dist/mcp/catalog.js.map +1 -0
- package/dist/mcp/config.d.ts +11 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +16 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +86 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +68 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +319 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/tools.d.ts +77 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +179 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/report/build.d.ts +8 -0
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +3 -1
- package/dist/report/build.js.map +1 -1
- package/dist/report/format.d.ts +19 -0
- package/dist/report/format.d.ts.map +1 -1
- package/dist/report/format.js +42 -0
- package/dist/report/format.js.map +1 -1
- package/dist/report/junit.d.ts.map +1 -1
- package/dist/report/junit.js +4 -1
- package/dist/report/junit.js.map +1 -1
- package/dist/report/list-explore.d.ts +109 -0
- package/dist/report/list-explore.d.ts.map +1 -0
- package/dist/report/list-explore.js +388 -0
- package/dist/report/list-explore.js.map +1 -0
- package/dist/report/list.d.ts +31 -1
- package/dist/report/list.d.ts.map +1 -1
- package/dist/report/list.js +114 -16
- package/dist/report/list.js.map +1 -1
- package/dist/report/running-tree.d.ts +7 -7
- package/dist/report/running-tree.d.ts.map +1 -1
- package/dist/report/running-tree.js +12 -13
- package/dist/report/running-tree.js.map +1 -1
- package/dist/run/app-ports.d.ts +17 -0
- package/dist/run/app-ports.d.ts.map +1 -0
- package/dist/run/app-ports.js +74 -0
- package/dist/run/app-ports.js.map +1 -0
- package/dist/run/events.d.ts +21 -7
- package/dist/run/events.d.ts.map +1 -1
- package/dist/run/events.js.map +1 -1
- package/dist/run/execute.d.ts +2 -2
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +42 -20
- package/dist/run/execute.js.map +1 -1
- package/dist/run/extended-fixtures.d.ts +33 -0
- package/dist/run/extended-fixtures.d.ts.map +1 -0
- package/dist/run/extended-fixtures.js +91 -0
- package/dist/run/extended-fixtures.js.map +1 -0
- package/dist/run/fixtures.d.ts +10 -4
- package/dist/run/fixtures.d.ts.map +1 -1
- package/dist/run/fixtures.js +40 -27
- package/dist/run/fixtures.js.map +1 -1
- package/dist/run/in-process.d.ts.map +1 -1
- package/dist/run/in-process.js +4 -3
- package/dist/run/in-process.js.map +1 -1
- package/dist/run/managed-process.d.ts +18 -0
- package/dist/run/managed-process.d.ts.map +1 -1
- package/dist/run/managed-process.js +101 -6
- package/dist/run/managed-process.js.map +1 -1
- package/dist/run/provision.d.ts +51 -0
- package/dist/run/provision.d.ts.map +1 -0
- package/dist/run/provision.js +94 -0
- package/dist/run/provision.js.map +1 -0
- package/dist/run/realm.d.ts +5 -4
- package/dist/run/realm.d.ts.map +1 -1
- package/dist/run/realm.js +8 -7
- package/dist/run/realm.js.map +1 -1
- package/dist/run/records.d.ts +4 -0
- package/dist/run/records.d.ts.map +1 -1
- package/dist/run/runner.d.ts +14 -5
- package/dist/run/runner.d.ts.map +1 -1
- package/dist/run/runner.js +28 -71
- package/dist/run/runner.js.map +1 -1
- package/dist/run/scheduler.d.ts +5 -2
- package/dist/run/scheduler.d.ts.map +1 -1
- package/dist/run/scheduler.js +13 -10
- package/dist/run/scheduler.js.map +1 -1
- package/dist/run/serial.d.ts +1 -1
- package/dist/run/serial.d.ts.map +1 -1
- package/dist/run/serial.js +7 -2
- package/dist/run/serial.js.map +1 -1
- package/dist/run/standalone.d.ts +63 -0
- package/dist/run/standalone.d.ts.map +1 -0
- package/dist/run/standalone.js +152 -0
- package/dist/run/standalone.js.map +1 -0
- package/dist/run/units.d.ts +6 -3
- package/dist/run/units.d.ts.map +1 -1
- package/dist/run/units.js +0 -0
- package/dist/run/units.js.map +1 -1
- package/dist/run/worker/entry.js +4 -3
- package/dist/run/worker/entry.js.map +1 -1
- package/dist/run/worker/protocol.d.ts +12 -1
- package/dist/run/worker/protocol.d.ts.map +1 -1
- package/dist/run/worker/protocol.js.map +1 -1
- package/dist/run/worker/session.d.ts +3 -4
- package/dist/run/worker/session.d.ts.map +1 -1
- package/dist/run/worker/session.js +5 -4
- package/dist/run/worker/session.js.map +1 -1
- package/dist/types.d.ts +77 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -7
- package/skills/e2e/SKILL.md +9 -7
- package/skills/e2e/references/agent.md +27 -12
- package/skills/e2e/references/debugging.md +5 -5
- package/skills/e2e/references/explore.md +15 -9
- package/skills/e2e/references/mcp.md +88 -0
- package/skills/e2e/references/running.md +23 -18
- package/skills/e2e/references/setup.md +32 -18
- package/skills/e2e/references/writing-tests.md +12 -5
- package/dist/agent/model/router.d.ts +0 -36
- package/dist/agent/model/router.d.ts.map +0 -1
- package/dist/agent/model/router.js +0 -45
- package/dist/agent/model/router.js.map +0 -1
- package/dist/explore/reporter.d.ts +0 -11
- package/dist/explore/reporter.d.ts.map +0 -1
- package/dist/explore/reporter.js +0 -54
- package/dist/explore/reporter.js.map +0 -1
package/dist/agent/act.js
CHANGED
|
@@ -1,76 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Harness-owned dispatch of one `agent.act()` step.
|
|
3
3
|
*
|
|
4
|
-
* The harness opens the step
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The harness opens the step and hands only the thinking to the configured
|
|
5
|
+
* executor; everything the executor can touch is built here from four
|
|
6
|
+
* collaborators, each owning one concern of the step:
|
|
7
|
+
*
|
|
8
|
+
* - `StepAccounting`: the clock, the budgets, the hard stop, the metrics.
|
|
9
|
+
* - `ObservationFeed`: every capture, the settle, the id ring, the pixel decision.
|
|
10
|
+
* - `ActionDispatcher`: the verb table, target resolution, recording.
|
|
11
|
+
* - `StepTraceSession`: replay, live recording, and the stage-or-evict decision.
|
|
12
|
+
*
|
|
13
|
+
* Capabilities beyond the grammar (secret fills, the trace cache) are modules
|
|
14
|
+
* over those collaborators, borrowed through a small host each. What stays in
|
|
15
|
+
* this file is the wiring, the executor's context, and the mapping of the
|
|
16
|
+
* executor's verdict onto the runner's error taxonomy.
|
|
11
17
|
*/
|
|
12
18
|
import { writeFileSync } from 'node:fs';
|
|
13
19
|
import { writeFile } from 'node:fs/promises';
|
|
14
20
|
import { join } from 'node:path';
|
|
15
|
-
import {
|
|
16
|
-
import { timestamp } from '../internal/ids.js';
|
|
17
|
-
import { resolveNavigationUrl } from '../internal/urls.js';
|
|
21
|
+
import { ConfigurationError } from '../internal/errors.js';
|
|
18
22
|
import { AgentError, CATEGORY_BY_CODE, isAgentError, toAgentError } from './error.js';
|
|
19
|
-
import { validateActOptions, validateInstruction, validateParams, validateVerdict
|
|
23
|
+
import { validateActOptions, validateInstruction, validateParams, validateVerdict } from './act-validation.js';
|
|
24
|
+
import { ActionDispatcher } from './action-dispatcher.js';
|
|
20
25
|
import { resolveBoundedBudget, resolveTimeout } from './call-options.js';
|
|
21
|
-
import { RUNTIME_CODES
|
|
22
|
-
import { isDerivedValue } from './derived.js';
|
|
26
|
+
import { RUNTIME_CODES } from './executor.js';
|
|
23
27
|
import { projectPriorSteps, serializeLedger } from './ledger.js';
|
|
24
28
|
import { instantiateLanguageModel } from './model/sdk.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { containerKey, describeAction, describeTarget } from './actions.js';
|
|
29
|
-
import { describePosition, relocateDescriptor } from '../cache/relocate.js';
|
|
30
|
-
import { authorizeSecretFill } from './secrets.js';
|
|
31
|
-
import { ModelUsage, tokenFields } from './usage.js';
|
|
29
|
+
import { ObservationFeed } from './observation-feed.js';
|
|
30
|
+
import { OperationQueue } from './operation-queue.js';
|
|
31
|
+
import { StepAccounting } from './step-accounting.js';
|
|
32
32
|
import { StepTraceSession } from './step-cache.js';
|
|
33
|
-
/**
|
|
34
|
-
* Ceiling on one targeted grammar action: the time the engine may wait for a
|
|
35
|
-
* node to become actionable before the failure goes back to the model.
|
|
36
|
-
* `actionTimeout` bounds every engine operation and doubles as the judgment
|
|
37
|
-
* tier's clock, so projects raise it for slow models; a tap under a consent
|
|
38
|
-
* overlay then sits in the engine's actionability retry for the whole budget
|
|
39
|
-
* (90 s in the testbed) before the model learns anything, when the useful
|
|
40
|
-
* answer — what is in the way — is known within seconds. Navigation keeps the
|
|
41
|
-
* full budget: a slow page really can take that long to load.
|
|
42
|
-
*/
|
|
43
|
-
const MAX_TARGETED_ACTION_MS = 15_000;
|
|
44
|
-
/**
|
|
45
|
-
* How many times a targeted action re-finds its node after the engine reports
|
|
46
|
-
* it stale. One relocation covers a re-render between the observation and the
|
|
47
|
-
* action; the second covers a render tick landing between the fresh look and
|
|
48
|
-
* the action itself. A control that keeps vanishing faster than that is
|
|
49
|
-
* reported to the model as gone.
|
|
50
|
-
*/
|
|
51
|
-
const MAX_STALE_RELOCATIONS = 2;
|
|
52
|
-
/**
|
|
53
|
-
* The change wait after a scroll or a mutating project tool. A scroll moves
|
|
54
|
-
* nothing the tree records and a tool usually changes state the screen shows
|
|
55
|
-
* only after a reload, so most of these change no shape at all and a long
|
|
56
|
-
* wait is pure cost; a windowed list rendering its next rows, or a tool the
|
|
57
|
-
* page reacts to, does so within a few hundred milliseconds.
|
|
58
|
-
*/
|
|
59
|
-
const BRIEF_CHANGE_WAIT_MS = 500;
|
|
60
|
-
/**
|
|
61
|
-
* Observations kept for resolving an id the newest one no longer carries.
|
|
62
|
-
* A turn that batches actions addresses the screen it saw, while every
|
|
63
|
-
* action's own look re-observes; on an engine that mints ids per observation
|
|
64
|
-
* (a device), each look renumbers the tree. A few looks back is as far as one
|
|
65
|
-
* turn can reach.
|
|
66
|
-
*/
|
|
67
|
-
const MAX_RECENT_OBSERVATIONS = 8;
|
|
68
|
-
/**
|
|
69
|
-
* How far one grammar scroll moves. The engine's default flick is half the
|
|
70
|
-
* scrolled box; a deliberate scroll of three quarters keeps every row on
|
|
71
|
-
* screen at least once while covering a long feed in fewer actions.
|
|
72
|
-
*/
|
|
73
|
-
const SCROLL_MOMENTUM = 'slow';
|
|
74
33
|
/** Runs one `agent.act()` call as a harness-dispatched executor step. */
|
|
75
34
|
export async function runActStep(runtime, instruction, options, extraArguments = 0) {
|
|
76
35
|
const normalized = validateInstruction(instruction, 'agent.act');
|
|
@@ -125,10 +84,10 @@ export async function runAssertStep(runtime, assertion, options) {
|
|
|
125
84
|
* passing is what confirms the traces staged before it at attempt end
|
|
126
85
|
* (cache/context.ts).
|
|
127
86
|
*/
|
|
128
|
-
async function dispatchAgentStep(runtime, spec
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
87
|
+
export async function dispatchAgentStep(runtime, spec,
|
|
88
|
+
// Resolved before the step opens, so an unknown name fails the call, not a
|
|
89
|
+
// recorded step, and the step carries the agent it ran with.
|
|
90
|
+
agent = runtime.select(spec.agent)) {
|
|
132
91
|
return runtime.steps.run('agent', spec.api, spec.instruction, async () => {
|
|
133
92
|
const dispatch = new ActDispatch(runtime, spec, agent);
|
|
134
93
|
try {
|
|
@@ -152,95 +111,49 @@ async function dispatchAgentStep(runtime, spec) {
|
|
|
152
111
|
}
|
|
153
112
|
}, { verifies: spec.kind === 'assert', agent: agent.name });
|
|
154
113
|
}
|
|
155
|
-
/**
|
|
156
|
-
* One step's harness-side state: budgets, the newest observation, and the
|
|
157
|
-
* fatal error (if any) that must outrank whatever the executor reports.
|
|
158
|
-
*/
|
|
114
|
+
/** One step's wiring: the collaborators, the executor's context, and the verdict mapping. */
|
|
159
115
|
class ActDispatch {
|
|
160
116
|
runtime;
|
|
161
117
|
spec;
|
|
162
118
|
agent;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
maxModelCalls;
|
|
167
|
-
metrics = {
|
|
168
|
-
modelCalls: 0,
|
|
169
|
-
actionSteps: 0,
|
|
170
|
-
observationBytes: 0,
|
|
171
|
-
contextBytes: 0,
|
|
172
|
-
ledgerBytes: 0,
|
|
173
|
-
};
|
|
174
|
-
latest;
|
|
175
|
-
/** The newest observations of the step, oldest first; see MAX_RECENT_OBSERVATIONS. */
|
|
176
|
-
recent = [];
|
|
177
|
-
explanation;
|
|
178
|
-
/** First budget/timeout/cancel failure; runtime truth outranks the verdict. */
|
|
179
|
-
hardStop;
|
|
180
|
-
/** Aborts the executor on any hard stop, so a step never outlives its clock. */
|
|
181
|
-
stepAbort = new AbortController();
|
|
182
|
-
/**
|
|
183
|
-
* The one signal everything inside the step aborts with: the attempt's
|
|
184
|
-
* cancellation or the step's own hard stop. Built once in the constructor
|
|
185
|
-
* so the executor context, the replay host, and the settle clock can never
|
|
186
|
-
* disagree about what "the step's signal" means.
|
|
187
|
-
*/
|
|
188
|
-
stepSignal;
|
|
189
|
-
/**
|
|
190
|
-
* Serializes observations and actions in call order. An executor (or an AI
|
|
191
|
-
* SDK loop running parallel tool calls) that issues a second action before
|
|
192
|
-
* the first settles would otherwise resolve both targets against the same
|
|
193
|
-
* pre-action observation — exactly the wrong-node hazard the staleness rule
|
|
194
|
-
* exists to prevent. Queued, the second call sees the newest observation
|
|
195
|
-
* and a stale id fails loud instead of acting on the wrong node.
|
|
196
|
-
*
|
|
197
|
-
* Invariant: a serialized body must never call `observe()` or `runAction()`
|
|
198
|
-
* itself — the inner call would queue behind its own caller and deadlock.
|
|
199
|
-
* Grammar bodies call the session directly, and secret authorization
|
|
200
|
-
* never observes; keep it that way.
|
|
201
|
-
*/
|
|
202
|
-
grammarChain = Promise.resolve();
|
|
203
|
-
sdkModel;
|
|
204
|
-
sdkModelResolved = false;
|
|
205
|
-
transcript;
|
|
206
|
-
/** Set by `finish()`: late executor accounting must not land on the next step. */
|
|
207
|
-
closed = false;
|
|
208
|
-
usage = new ModelUsage();
|
|
209
|
-
modelProvider;
|
|
210
|
-
modelId;
|
|
211
|
-
redact;
|
|
119
|
+
accounting;
|
|
120
|
+
feed;
|
|
121
|
+
dispatcher;
|
|
212
122
|
/** The step's trace-cache session; undefined when caching is off or the kind is not cacheable. */
|
|
213
123
|
stepCache;
|
|
214
124
|
/** Timeline index of the step being dispatched. */
|
|
215
125
|
stepIndex;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
* until the next settled observation has waited for the screen to leave it.
|
|
221
|
-
* Armed by actions whose effect shows in the tree; a secret fill leaves no
|
|
222
|
-
* visible trace and a scroll moves nothing the tree records, so neither
|
|
223
|
-
* arms it. Without this, the observation after a tap on a link reads the
|
|
224
|
-
* old page, stable and wrong, and the model repairs what already worked.
|
|
225
|
-
*/
|
|
226
|
-
pendingChange;
|
|
227
|
-
/** The change wait the pending action asked for; undefined takes the default. */
|
|
228
|
-
pendingChangeWaitMs;
|
|
126
|
+
explanation;
|
|
127
|
+
transcript;
|
|
128
|
+
sdkModel;
|
|
129
|
+
sdkModelResolved = false;
|
|
229
130
|
constructor(runtime, spec,
|
|
230
131
|
/** The agent this step runs with. */
|
|
231
132
|
agent) {
|
|
232
133
|
this.runtime = runtime;
|
|
233
134
|
this.spec = spec;
|
|
234
135
|
this.agent = agent;
|
|
235
|
-
this.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
136
|
+
this.accounting = new StepAccounting(runtime, {
|
|
137
|
+
api: spec.api,
|
|
138
|
+
timeoutMs: resolveTimeout(spec.timeout, runtime.config.timeout),
|
|
139
|
+
maxActions: resolveBoundedBudget(spec.maxSteps, agent.config.maxSteps, 'maxSteps'),
|
|
140
|
+
maxModelCalls: resolveBoundedBudget(spec.maxModelCalls, agent.config.maxModelCalls, 'maxModelCalls'),
|
|
141
|
+
contextBytes: new TextEncoder().encode(agent.agentContext ?? '').byteLength,
|
|
142
|
+
});
|
|
143
|
+
// One queue for observations and actions alike: call order is what keeps
|
|
144
|
+
// a batched turn from resolving two targets against one stale screen.
|
|
145
|
+
const queue = new OperationQueue();
|
|
146
|
+
this.feed = new ObservationFeed(runtime, this.accounting, queue, {
|
|
147
|
+
maxObservationBytes: agent.config.maxObservationBytes,
|
|
148
|
+
});
|
|
149
|
+
this.dispatcher = new ActionDispatcher(runtime, this.accounting, this.feed, queue, {
|
|
150
|
+
instruction: spec.instruction,
|
|
151
|
+
params: spec.params,
|
|
152
|
+
secrets: spec.secrets,
|
|
153
|
+
trace: () => this.stepCache,
|
|
154
|
+
});
|
|
155
|
+
// The dispatch always runs inside a recorded step (`dispatchAgentStep`
|
|
156
|
+
// opens one); the index names the step to the executor and to the trace cache.
|
|
244
157
|
const stepIndex = runtime.steps.currentStepIndex;
|
|
245
158
|
if (stepIndex === undefined)
|
|
246
159
|
throw new Error('agent step dispatched outside a recorded step');
|
|
@@ -258,13 +171,11 @@ class ActDispatch {
|
|
|
258
171
|
params: spec.params,
|
|
259
172
|
executor: {
|
|
260
173
|
name: agent.executor.name,
|
|
261
|
-
...(agent.executor.version === undefined
|
|
262
|
-
? {}
|
|
263
|
-
: { version: agent.executor.version }),
|
|
174
|
+
...(agent.executor.version === undefined ? {} : { version: agent.executor.version }),
|
|
264
175
|
},
|
|
265
|
-
redact:
|
|
176
|
+
redact: runtime.redact,
|
|
266
177
|
testIdAttribute: runtime.config.testIdAttribute,
|
|
267
|
-
maxActions: this.maxActions,
|
|
178
|
+
maxActions: this.accounting.maxActions,
|
|
268
179
|
stepIndex,
|
|
269
180
|
});
|
|
270
181
|
}
|
|
@@ -274,7 +185,7 @@ class ActDispatch {
|
|
|
274
185
|
// oxlint-disable-next-line typescript/no-this-alias
|
|
275
186
|
const dispatch = this;
|
|
276
187
|
const ledger = serializeLedger(projectPriorSteps(this.runtime.priorSteps()), this.runtime.config.limits.maxLedgerBytes);
|
|
277
|
-
this.metrics.ledgerBytes = ledger.bytes;
|
|
188
|
+
this.accounting.metrics.ledgerBytes = ledger.bytes;
|
|
278
189
|
const replayedPrefix = this.stepCache?.replayedPrefix;
|
|
279
190
|
return {
|
|
280
191
|
step: {
|
|
@@ -290,7 +201,7 @@ class ActDispatch {
|
|
|
290
201
|
target: this.runtime.target,
|
|
291
202
|
attempt: this.runtime.attempt,
|
|
292
203
|
...(replayedPrefix === undefined ? {} : { replayedPrefix }),
|
|
293
|
-
signal: this.
|
|
204
|
+
signal: this.accounting.signal,
|
|
294
205
|
// Resolved on first read, so executors that bring their own model (or
|
|
295
206
|
// none) never pay for — or fail on — config model resolution.
|
|
296
207
|
get model() {
|
|
@@ -300,14 +211,17 @@ class ActDispatch {
|
|
|
300
211
|
ledger: ledger.text,
|
|
301
212
|
agentContext: this.agent.agentContext,
|
|
302
213
|
budgets: {
|
|
303
|
-
maxActions: this.maxActions,
|
|
304
|
-
maxModelCalls: this.maxModelCalls,
|
|
305
|
-
actionsUsed: () => this.metrics.actionSteps,
|
|
306
|
-
remainingMs: () => this.
|
|
307
|
-
recordModelCall: (usage) => this.recordModelCall(usage),
|
|
308
|
-
runTool: (call, body) => this.runTool(call, body),
|
|
214
|
+
maxActions: this.accounting.maxActions,
|
|
215
|
+
maxModelCalls: this.accounting.maxModelCalls,
|
|
216
|
+
actionsUsed: () => this.accounting.metrics.actionSteps,
|
|
217
|
+
remainingMs: () => this.accounting.remainingMs(),
|
|
218
|
+
recordModelCall: (usage) => this.accounting.recordModelCall(usage),
|
|
219
|
+
runTool: (call, body) => this.dispatcher.runTool(call, body),
|
|
220
|
+
},
|
|
221
|
+
observe: (options) => this.feed.observe(options),
|
|
222
|
+
get pixelsTainted() {
|
|
223
|
+
return dispatch.runtime.taint.value;
|
|
309
224
|
},
|
|
310
|
-
observe: (options) => this.observe(options),
|
|
311
225
|
attachTranscript: (text) => {
|
|
312
226
|
// Debug detail only: transcripts are model prose and can be large.
|
|
313
227
|
if (this.runtime.debug?.enabled === true && typeof text === 'string' && text !== '') {
|
|
@@ -315,53 +229,7 @@ class ActDispatch {
|
|
|
315
229
|
}
|
|
316
230
|
},
|
|
317
231
|
attachScreenshot: (pixels, label) => this.attachScreenshot(pixels, label),
|
|
318
|
-
actions: this.
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* The action grammar, shared verbatim by the executor context and the
|
|
323
|
-
* replay engine: a replayed action runs under exactly the same deadline,
|
|
324
|
-
* budget, policy, and recording as a live one. Committed actions are
|
|
325
|
-
* recorded into the step trace with the node they actually acted on —
|
|
326
|
-
* capture at commit time is what makes the descriptor durable evidence
|
|
327
|
-
* rather than a guess.
|
|
328
|
-
*/
|
|
329
|
-
buildActions() {
|
|
330
|
-
return {
|
|
331
|
-
tap: (target) => this.commitTargeted('tap', target, async (node) => {
|
|
332
|
-
await this.session.perform(node.ref, { kind: 'tap' }, this.actionOperation());
|
|
333
|
-
return { name: 'tap', node };
|
|
334
|
-
}),
|
|
335
|
-
type: (target, value) => {
|
|
336
|
-
if (typeof value !== 'string') {
|
|
337
|
-
throw new TestError('INVALID_ARGUMENT', 'type value must be a string');
|
|
338
|
-
}
|
|
339
|
-
return this.commitTargeted('type', target, async (node) => {
|
|
340
|
-
await this.session.perform(node.ref, { kind: 'fill', value, sensitive: false }, this.actionOperation());
|
|
341
|
-
return { name: 'type', node, value };
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
typeSecret: (target, name) => this.typeSecret(target, name),
|
|
345
|
-
press: (target, key) => {
|
|
346
|
-
if (typeof key !== 'string' || key.trim() === '' || key.length > 64) {
|
|
347
|
-
throw new TestError('INVALID_ARGUMENT', 'press key must be a short non-empty string');
|
|
348
|
-
}
|
|
349
|
-
return this.commitTargeted('press', target, async (node) => {
|
|
350
|
-
await this.session.perform(node.ref, { kind: 'press', key }, this.actionOperation());
|
|
351
|
-
return { name: 'press', node, key };
|
|
352
|
-
});
|
|
353
|
-
},
|
|
354
|
-
select: (target, value) => {
|
|
355
|
-
if (typeof value !== 'string' || value === '') {
|
|
356
|
-
throw new TestError('INVALID_ARGUMENT', 'select value must be a non-empty option label');
|
|
357
|
-
}
|
|
358
|
-
return this.commitTargeted('selectOption', target, async (node) => {
|
|
359
|
-
await this.session.perform(node.ref, { kind: 'selectOption', value }, this.actionOperation());
|
|
360
|
-
return { name: 'select', node, value };
|
|
361
|
-
});
|
|
362
|
-
},
|
|
363
|
-
scroll: (direction, target) => this.scroll(direction, target),
|
|
364
|
-
navigate: (url) => this.navigate(url),
|
|
232
|
+
actions: this.dispatcher.actions,
|
|
365
233
|
};
|
|
366
234
|
}
|
|
367
235
|
/**
|
|
@@ -372,14 +240,15 @@ class ActDispatch {
|
|
|
372
240
|
* the promise is abandoned, never awaited past the deadline.
|
|
373
241
|
*/
|
|
374
242
|
async run() {
|
|
243
|
+
const { accounting } = this;
|
|
375
244
|
const timer = setTimeout(() => {
|
|
376
|
-
|
|
377
|
-
}, Math.max(1,
|
|
245
|
+
accounting.fatalize(new AgentError('STEP_TIMEOUT', `${this.spec.api} exceeded its ${accounting.timeoutMs} ms timeout`));
|
|
246
|
+
}, Math.max(1, accounting.remainingMs()));
|
|
378
247
|
try {
|
|
379
248
|
return await Promise.race([
|
|
380
249
|
this.dispatchStep(),
|
|
381
250
|
new Promise((_, reject) => {
|
|
382
|
-
const signal =
|
|
251
|
+
const { signal } = accounting;
|
|
383
252
|
if (signal.aborted) {
|
|
384
253
|
reject(signal.reason);
|
|
385
254
|
return;
|
|
@@ -392,39 +261,6 @@ class ActDispatch {
|
|
|
392
261
|
clearTimeout(timer);
|
|
393
262
|
}
|
|
394
263
|
}
|
|
395
|
-
async dispatchStep() {
|
|
396
|
-
const replayed = await this.stepCache?.begin();
|
|
397
|
-
if (replayed !== undefined)
|
|
398
|
-
return replayed;
|
|
399
|
-
return this.agent.executor.runStep(this.context());
|
|
400
|
-
}
|
|
401
|
-
/** The cache session's narrow view of this dispatch. */
|
|
402
|
-
cacheHost() {
|
|
403
|
-
return {
|
|
404
|
-
observe: async () => (await this.observeLatest()).nodes,
|
|
405
|
-
observeSettled: async () => (await this.observeSettled()).nodes,
|
|
406
|
-
actions: this.buildActions(),
|
|
407
|
-
signal: this.stepSignal,
|
|
408
|
-
remainingMs: () => this.deadline.remaining(),
|
|
409
|
-
redact: this.redact,
|
|
410
|
-
testIdAttribute: this.runtime.config.testIdAttribute,
|
|
411
|
-
currentPath: (nodes) => this.currentPath(nodes !== undefined && nodes === this.latest?.nodes ? this.latest : undefined),
|
|
412
|
-
replaying: (active) => this.runtime.steps.replaying(active),
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
/** Best-effort current location path + query, for trace preconditions. */
|
|
416
|
-
async currentPath(observation) {
|
|
417
|
-
const currentUrl = this.session.url;
|
|
418
|
-
if (observation?.url === undefined && currentUrl === undefined)
|
|
419
|
-
return undefined;
|
|
420
|
-
try {
|
|
421
|
-
const url = new URL(observation?.url ?? await currentUrl(this.operation()));
|
|
422
|
-
return `${url.pathname}${url.search}`;
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
264
|
/** Hands the settled outcome to the cache session, which stages, evicts, or does nothing. */
|
|
429
265
|
async conclude(outcome) {
|
|
430
266
|
await this.stepCache?.conclude(outcome, this.explanation);
|
|
@@ -432,27 +268,24 @@ class ActDispatch {
|
|
|
432
268
|
/** Maps the executor's verdict onto the runner outcome. Fail-closed on hard stops. */
|
|
433
269
|
settle(verdict) {
|
|
434
270
|
let settled = verdict;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
271
|
+
const hardStop = this.accounting.hardStop;
|
|
272
|
+
if (hardStop !== undefined) {
|
|
273
|
+
if (hardStop.code === 'CANCELLED')
|
|
274
|
+
throw hardStop;
|
|
438
275
|
if (settled.status === 'passed') {
|
|
439
276
|
// The step ran out of budget or time mid-flight; an executor cannot
|
|
440
277
|
// declare success over the runtime's own accounting.
|
|
441
|
-
settled = {
|
|
442
|
-
status: 'blocked',
|
|
443
|
-
summary: this.hardStop.message,
|
|
444
|
-
errorCode: this.hardStop.code,
|
|
445
|
-
};
|
|
278
|
+
settled = { status: 'blocked', summary: hardStop.message, errorCode: hardStop.code };
|
|
446
279
|
}
|
|
447
280
|
else if (settled.errorCode === undefined) {
|
|
448
281
|
// The executor's analysis stands, but runtime exhaustion is never
|
|
449
282
|
// hidden from the report: a code-less failure inherits the hard stop.
|
|
450
|
-
settled = { ...settled, errorCode:
|
|
283
|
+
settled = { ...settled, errorCode: hardStop.code };
|
|
451
284
|
}
|
|
452
285
|
}
|
|
453
286
|
if (settled.errorCode !== undefined &&
|
|
454
287
|
RUNTIME_CODES.has(settled.errorCode) &&
|
|
455
|
-
!this.vouches(settled.errorCode)) {
|
|
288
|
+
!this.accounting.vouches(settled.errorCode)) {
|
|
456
289
|
throw this.invented(settled.errorCode);
|
|
457
290
|
}
|
|
458
291
|
this.explanation = settled.summary;
|
|
@@ -473,132 +306,66 @@ class ActDispatch {
|
|
|
473
306
|
settleThrown(error) {
|
|
474
307
|
if (!RUNTIME_CODES.has(error.code))
|
|
475
308
|
return error;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
309
|
+
const hardStop = this.accounting.hardStop;
|
|
310
|
+
if (hardStop !== undefined)
|
|
311
|
+
return hardStop;
|
|
312
|
+
if (this.accounting.vouches(error.code))
|
|
479
313
|
return error;
|
|
480
314
|
return this.invented(error.code, error);
|
|
481
315
|
}
|
|
482
|
-
/**
|
|
483
|
-
* Whether the runtime's own accounting corroborates a runtime code. Codes
|
|
484
|
-
* are runtime-assigned: the recorded hard stop vouches directly, and the
|
|
485
|
-
* clock, the budgets, and the abort signal vouch for an executor that
|
|
486
|
-
* observed exhaustion before the context machinery did.
|
|
487
|
-
*/
|
|
488
|
-
vouches(code) {
|
|
489
|
-
if (this.hardStop?.code === code)
|
|
490
|
-
return true;
|
|
491
|
-
switch (code) {
|
|
492
|
-
case 'CANCELLED':
|
|
493
|
-
return this.runtime.engine.signal.aborted;
|
|
494
|
-
case 'STEP_TIMEOUT':
|
|
495
|
-
return this.deadline.expired();
|
|
496
|
-
case 'STEP_BUDGET_EXHAUSTED':
|
|
497
|
-
return (this.metrics.actionSteps >= this.maxActions ||
|
|
498
|
-
this.metrics.modelCalls >= this.maxModelCalls);
|
|
499
|
-
default:
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
invented(code, cause) {
|
|
504
|
-
return new AgentError('MODEL_OUTPUT_INVALID', `executor "${this.agent.executor.name}" reported runtime code ${code}, which the runtime never assigned`, cause === undefined ? {} : { cause });
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Records the first hard stop. Timeout and cancellation also abort the step
|
|
508
|
-
* signal — there is nothing left for the executor to say. Budget exhaustion
|
|
509
|
-
* does not: the executor may still catch it and conclude with its own
|
|
510
|
-
* analysis (which `settle` stamps with the runtime code), bounded by the
|
|
511
|
-
* deadline either way.
|
|
512
|
-
*/
|
|
513
|
-
fatalize(error) {
|
|
514
|
-
this.hardStop ??= error;
|
|
515
|
-
if (error.code !== 'STEP_BUDGET_EXHAUSTED' && !this.stepAbort.signal.aborted) {
|
|
516
|
-
this.stepAbort.abort(this.hardStop);
|
|
517
|
-
}
|
|
518
|
-
return error;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Counts and accounts one executor-made model call. The budget is enforced:
|
|
522
|
-
* the call past the limit records, then hard-stops the step.
|
|
523
|
-
*/
|
|
524
|
-
recordModelCall(usage) {
|
|
525
|
-
// An executor abandoned by a hard stop can still report late; the step it
|
|
526
|
-
// belonged to is closed, and the recorder's active step is now another.
|
|
527
|
-
if (this.closed)
|
|
528
|
-
return;
|
|
529
|
-
this.metrics.modelCalls += 1;
|
|
530
|
-
const tokens = this.usage.record(usage);
|
|
531
|
-
if (usage?.provider !== undefined)
|
|
532
|
-
this.modelProvider = usage.provider;
|
|
533
|
-
if (usage?.modelId !== undefined)
|
|
534
|
-
this.modelId = usage.modelId;
|
|
535
|
-
this.runtime.steps.recordEvent({
|
|
536
|
-
kind: 'model',
|
|
537
|
-
startedAt: usage?.startedAt ?? timestamp(),
|
|
538
|
-
durationMs: Math.max(0, Math.round(usage?.durationMs ?? 0)),
|
|
539
|
-
status: 'passed',
|
|
540
|
-
name: 'executor',
|
|
541
|
-
count: tokens,
|
|
542
|
-
...tokenFields(usage),
|
|
543
|
-
});
|
|
544
|
-
this.runtime.debug?.record('agent.model', Math.max(0, Math.round(usage?.durationMs ?? 0)));
|
|
545
|
-
if (this.metrics.modelCalls > this.maxModelCalls) {
|
|
546
|
-
throw this.fatalize(new AgentError('STEP_BUDGET_EXHAUSTED', `${this.spec.api} exhausted its model-call budget of ${this.maxModelCalls}`));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/** Claims a mutation slot before any side effect, for both grammar and project tools. */
|
|
550
|
-
reserveAction() {
|
|
551
|
-
this.checkpoint();
|
|
552
|
-
if (this.closed)
|
|
553
|
-
throw new AgentError('CANCELLED', 'the step has ended');
|
|
554
|
-
if (this.metrics.actionSteps >= this.maxActions) {
|
|
555
|
-
throw this.fatalize(new AgentError('STEP_BUDGET_EXHAUSTED', `${this.spec.api} exhausted its action budget of ${this.maxActions}`));
|
|
556
|
-
}
|
|
557
|
-
// The budget slot is consumed either way: a failed dispatch was an attempt.
|
|
558
|
-
this.metrics.actionSteps += 1;
|
|
559
|
-
}
|
|
560
|
-
/** Records project tools through the same budget and operation queue as grammar actions. */
|
|
561
|
-
runTool(call, body) {
|
|
562
|
-
const run = async () => {
|
|
563
|
-
this.checkpoint();
|
|
564
|
-
if (this.closed)
|
|
565
|
-
throw new AgentError('CANCELLED', 'the step has ended');
|
|
566
|
-
if (call.mutates) {
|
|
567
|
-
this.reserveAction();
|
|
568
|
-
this.stepCache?.recordGap(call.name);
|
|
569
|
-
}
|
|
570
|
-
const value = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'engine', phase: 'agent.action', name: `tool:${call.name}` }, body);
|
|
571
|
-
// A tool the page reacts to at once is read after the reaction; one
|
|
572
|
-
// whose effect shows only after a reload costs the brief wait, not two seconds.
|
|
573
|
-
if (call.mutates)
|
|
574
|
-
this.armChange(BRIEF_CHANGE_WAIT_MS);
|
|
575
|
-
return value;
|
|
576
|
-
};
|
|
577
|
-
return call.mutates ? this.serialized(run) : run();
|
|
578
|
-
}
|
|
579
316
|
/** What the settled step did, for the caller of `agent.act()`. */
|
|
580
317
|
result() {
|
|
581
318
|
const cache = this.stepCache?.cacheInfo;
|
|
582
319
|
return {
|
|
583
320
|
summary: this.explanation ?? '',
|
|
584
321
|
...(cache === undefined ? {} : { cache }),
|
|
585
|
-
modelCalls: this.metrics.modelCalls,
|
|
586
|
-
actions: this.metrics.actionSteps,
|
|
322
|
+
modelCalls: this.accounting.metrics.modelCalls,
|
|
323
|
+
actions: this.accounting.metrics.actionSteps,
|
|
587
324
|
};
|
|
588
325
|
}
|
|
589
|
-
/**
|
|
326
|
+
/** Closes the books and attaches metrics, model provenance, and the verdict explanation to the step. */
|
|
590
327
|
finish() {
|
|
591
|
-
this.
|
|
328
|
+
this.accounting.close();
|
|
329
|
+
const { metrics } = this.accounting;
|
|
592
330
|
const cacheInfo = this.stepCache?.cacheInfo;
|
|
331
|
+
const latest = this.feed.latest;
|
|
332
|
+
const model = this.accounting.modelInfo(this.agent.executor);
|
|
593
333
|
this.runtime.steps.attachAgentDetails({
|
|
594
|
-
metrics: { ...
|
|
595
|
-
...(
|
|
334
|
+
metrics: { ...metrics },
|
|
335
|
+
...(model === undefined ? {} : { model }),
|
|
596
336
|
...(cacheInfo === undefined ? {} : { cache: cacheInfo }),
|
|
597
337
|
...(this.explanation !== undefined ? { explanation: this.explanation } : {}),
|
|
598
|
-
...(
|
|
338
|
+
...(latest !== undefined ? { observationRevision: latest.revision } : {}),
|
|
339
|
+
...this.feed.visionReport(),
|
|
599
340
|
});
|
|
600
341
|
this.writeTranscript();
|
|
601
342
|
}
|
|
343
|
+
async dispatchStep() {
|
|
344
|
+
const replayed = await this.stepCache?.begin();
|
|
345
|
+
if (replayed !== undefined)
|
|
346
|
+
return replayed;
|
|
347
|
+
return this.agent.executor.runStep(this.context());
|
|
348
|
+
}
|
|
349
|
+
/** The cache session's narrow view of this step. */
|
|
350
|
+
cacheHost() {
|
|
351
|
+
return {
|
|
352
|
+
observe: async () => screenOf(await this.feed.observeLatest()),
|
|
353
|
+
observeSettled: async () => screenOf(await this.feed.observeSettled()),
|
|
354
|
+
actions: this.dispatcher.actions,
|
|
355
|
+
signal: this.accounting.signal,
|
|
356
|
+
remainingMs: () => this.accounting.remainingMs(),
|
|
357
|
+
redact: this.runtime.redact,
|
|
358
|
+
testIdAttribute: this.runtime.config.testIdAttribute,
|
|
359
|
+
currentPath: (nodes) => {
|
|
360
|
+
const latest = this.feed.latest;
|
|
361
|
+
return this.feed.currentPath(nodes !== undefined && nodes === latest?.nodes ? latest : undefined);
|
|
362
|
+
},
|
|
363
|
+
replaying: (active) => this.runtime.steps.replaying(active),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
invented(code, cause) {
|
|
367
|
+
return new AgentError('MODEL_OUTPUT_INVALID', `executor "${this.agent.executor.name}" reported runtime code ${code}, which the runtime never assigned`, cause === undefined ? {} : { cause });
|
|
368
|
+
}
|
|
602
369
|
/**
|
|
603
370
|
* Persists observed pixels as a step-attributed `screenshot` artifact and
|
|
604
371
|
* returns its id. The pixels are the engine's redacted capture, so the file
|
|
@@ -620,39 +387,13 @@ class ActDispatch {
|
|
|
620
387
|
try {
|
|
621
388
|
// Model prose and project-tool output are not model input, but they
|
|
622
389
|
// are a log: the same redactor that guards the tree guards the file.
|
|
623
|
-
writeFileSync(join(this.runtime.artifacts.dir, name), this.redact(this.transcript), 'utf8');
|
|
390
|
+
writeFileSync(join(this.runtime.artifacts.dir, name), this.runtime.redact(this.transcript), 'utf8');
|
|
624
391
|
this.runtime.steps.attachArtifact(this.runtime.artifacts.register('log', name));
|
|
625
392
|
}
|
|
626
393
|
catch {
|
|
627
394
|
// The transcript is best-effort debug detail; never fail the step for it.
|
|
628
395
|
}
|
|
629
396
|
}
|
|
630
|
-
/**
|
|
631
|
-
* Model provenance for the report. The executor is the authority on which
|
|
632
|
-
* model answered; an executor that reports nothing is still identified, so a
|
|
633
|
-
* step's model calls are never attributed to the wrong tier.
|
|
634
|
-
*/
|
|
635
|
-
modelInfo() {
|
|
636
|
-
const executor = this.agent.executor;
|
|
637
|
-
const executorVersion = executor.version ?? '0';
|
|
638
|
-
return this.usage.report({
|
|
639
|
-
provider: this.modelProvider ?? executor.name,
|
|
640
|
-
model: this.modelId ?? executor.name,
|
|
641
|
-
endpoint: 'provider-default',
|
|
642
|
-
adapterVersion: `executor/${executor.name}@${executorVersion}`,
|
|
643
|
-
policyVersion: `${executor.name}/${executorVersion}`,
|
|
644
|
-
}, this.metrics.modelCalls);
|
|
645
|
-
}
|
|
646
|
-
get session() {
|
|
647
|
-
return this.runtime.engine.session;
|
|
648
|
-
}
|
|
649
|
-
operation() {
|
|
650
|
-
return boundedOperation(this.runtime.engine, this.runtime.config.actionTimeout, this.deadline);
|
|
651
|
-
}
|
|
652
|
-
/** The operation context of one targeted action; see MAX_TARGETED_ACTION_MS. */
|
|
653
|
-
actionOperation() {
|
|
654
|
-
return boundedOperation(this.runtime.engine, Math.min(this.runtime.config.actionTimeout, MAX_TARGETED_ACTION_MS), this.deadline);
|
|
655
|
-
}
|
|
656
397
|
/** Resolves the configured model once; executors that never read it never pay. */
|
|
657
398
|
resolveModel() {
|
|
658
399
|
if (!this.sdkModelResolved) {
|
|
@@ -662,358 +403,9 @@ class ActDispatch {
|
|
|
662
403
|
}
|
|
663
404
|
return this.sdkModel;
|
|
664
405
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
signal: this.runtime.engine.signal,
|
|
670
|
-
deadline: this.deadline,
|
|
671
|
-
api: this.spec.api,
|
|
672
|
-
timeoutMs: this.timeoutMs,
|
|
673
|
-
...(cause === undefined ? {} : { cause }),
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
catch (error) {
|
|
677
|
-
if (isAgentError(error))
|
|
678
|
-
this.fatalize(error);
|
|
679
|
-
throw error;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
/** Chains one grammar operation behind every earlier one, in call order. */
|
|
683
|
-
serialized(body) {
|
|
684
|
-
// The chain is always already settled-to-undefined, so failures propagate
|
|
685
|
-
// to their own caller and never poison the queue.
|
|
686
|
-
const run = this.grammarChain.then(body);
|
|
687
|
-
this.grammarChain = run.then(() => undefined, () => undefined);
|
|
688
|
-
return run;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* The executor-facing observe: always settled. An executor observation is
|
|
692
|
-
* followed by a model call measured in seconds, so the bounded settle wait
|
|
693
|
-
* is noise there — and it guarantees the model never reads a snapshot the
|
|
694
|
-
* app is still reacting to, which a fast model turns into a repeated action
|
|
695
|
-
* (double-committing a toggle) and a verdict judged on pre-render state.
|
|
696
|
-
* Replay's pre-action looks and the cache session's probes settle through
|
|
697
|
-
* the same path (`observeSettled`); only replay's polls between retries
|
|
698
|
-
* read raw (`observeLatest`).
|
|
699
|
-
*/
|
|
700
|
-
async observe(options = {}) {
|
|
701
|
-
if (options === null || typeof options !== 'object') {
|
|
702
|
-
throw new TestError('INVALID_ARGUMENT', 'observe options must be an object');
|
|
703
|
-
}
|
|
704
|
-
const wantPixels = options.pixels === true;
|
|
705
|
-
const observation = await this.observeSettled(wantPixels);
|
|
706
|
-
// Prefer location from this capture; only engines without it need a separate probe.
|
|
707
|
-
const path = await this.currentPath(observation);
|
|
708
|
-
return {
|
|
709
|
-
revision: observation.revision,
|
|
710
|
-
text: observation.text,
|
|
711
|
-
truncated: observation.truncated,
|
|
712
|
-
viewport: observation.viewport,
|
|
713
|
-
...(path === undefined ? {} : { path: this.redact(path) }),
|
|
714
|
-
...(options.tree === true ? { tree: projectTree(observation.tree, this.redact) } : {}),
|
|
715
|
-
...(wantPixels ? this.pixelsFor(observation) : {}),
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Pixels for an executor that asked for them, or the reason they are
|
|
720
|
-
* withheld — the same decision the judgment tier makes, recorded as a
|
|
721
|
-
* policy event whenever it changes within the step.
|
|
722
|
-
*/
|
|
723
|
-
pixelsFor(observation) {
|
|
724
|
-
const outcome = pixelsForModel(observation, this.runtime.taint.value);
|
|
725
|
-
if ('withheld' in outcome) {
|
|
726
|
-
this.recordPixelDecision('denied', outcome.withheld);
|
|
727
|
-
return { pixelsWithheld: outcome.withheld };
|
|
728
|
-
}
|
|
729
|
-
this.recordPixelDecision('allowed');
|
|
730
|
-
this.metrics.pixelBytes = Math.max(this.metrics.pixelBytes ?? 0, outcome.pixels.data.byteLength);
|
|
731
|
-
return { pixels: outcome.pixels };
|
|
732
|
-
}
|
|
733
|
-
recordPixelDecision(decision, code) {
|
|
734
|
-
const key = code ?? decision;
|
|
735
|
-
if (this.pixelsDecided === key)
|
|
736
|
-
return;
|
|
737
|
-
this.pixelsDecided = key;
|
|
738
|
-
this.recordPolicy('vision.pixels', decision, code);
|
|
739
|
-
}
|
|
740
|
-
observeLatest() {
|
|
741
|
-
return this.serialized(() => this.observeNow(false, false));
|
|
742
|
-
}
|
|
743
|
-
observeSettled(pixels = false) {
|
|
744
|
-
return this.serialized(() => this.observeNow(true, pixels));
|
|
745
|
-
}
|
|
746
|
-
/** One recorded observation; when `settle`, the captures loop inside it. */
|
|
747
|
-
async observeNow(settle, pixels) {
|
|
748
|
-
this.checkpoint();
|
|
749
|
-
// A tainted viewport never captures pixels: the engine would mask what it
|
|
750
|
-
// knows about, and the secret may be anywhere on screen by now.
|
|
751
|
-
const capturePixels = pixels && !this.runtime.taint.value;
|
|
752
|
-
// A settled look consumes the pending change: it waits for the screen to
|
|
753
|
-
// leave the pre-action shape once, and later looks read the screen as is.
|
|
754
|
-
const changedFrom = settle ? this.pendingChange : undefined;
|
|
755
|
-
const changeWaitMs = settle ? this.pendingChangeWaitMs : undefined;
|
|
756
|
-
if (settle) {
|
|
757
|
-
this.pendingChange = undefined;
|
|
758
|
-
this.pendingChangeWaitMs = undefined;
|
|
759
|
-
}
|
|
760
|
-
const observation = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'observation', phase: 'agent.observe' }, () => settle
|
|
761
|
-
? settleObservation(() => this.captureObservation(capturePixels), observationShape, {
|
|
762
|
-
remainingMs: () => this.deadline.remaining(),
|
|
763
|
-
// The step's own hard stop must interrupt a settle sleep too —
|
|
764
|
-
// the attempt signal alone would let settling outlive the step
|
|
765
|
-
// by one poll interval.
|
|
766
|
-
signal: this.stepSignal,
|
|
767
|
-
}, { changedFrom, changeWaitMs, transitional: isTransitionalObservation })
|
|
768
|
-
: this.captureObservation(capturePixels), (prepared) => ({ count: prepared.nodes.size, bytes: prepared.bytes }));
|
|
769
|
-
this.publish(observation);
|
|
770
|
-
this.metrics.observationBytes = Math.max(this.metrics.observationBytes, observation.bytes);
|
|
771
|
-
return observation;
|
|
772
|
-
}
|
|
773
|
-
/** Makes an observation the newest and remembers it among the recent ones. */
|
|
774
|
-
publish(observation) {
|
|
775
|
-
this.latest = observation;
|
|
776
|
-
this.recent.push(observation);
|
|
777
|
-
if (this.recent.length > MAX_RECENT_OBSERVATIONS)
|
|
778
|
-
this.recent.shift();
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Arms the change wait: the next settled observation waits for the screen
|
|
782
|
-
* to leave the newest observation's shape, for the default two seconds or
|
|
783
|
-
* the given brief window.
|
|
784
|
-
*/
|
|
785
|
-
armChange(waitMs) {
|
|
786
|
-
if (this.latest === undefined)
|
|
787
|
-
return;
|
|
788
|
-
this.pendingChange = observationShape(this.latest);
|
|
789
|
-
this.pendingChangeWaitMs = waitMs;
|
|
790
|
-
}
|
|
791
|
-
/** One raw observation capture: retried at the engine, then redacted and bounded. */
|
|
792
|
-
async captureObservation(pixels) {
|
|
793
|
-
const raw = await retryingObserve({
|
|
794
|
-
observe: (operation) => this.session.observe(operation, { pixels }),
|
|
795
|
-
operation: () => this.operation(),
|
|
796
|
-
guard: (cause) => this.checkpoint(cause),
|
|
797
|
-
signal: this.runtime.engine.signal,
|
|
798
|
-
api: this.spec.api,
|
|
799
|
-
});
|
|
800
|
-
return prepareObservation(raw, {
|
|
801
|
-
redact: this.runtime.redact,
|
|
802
|
-
maxBytes: this.observationByteBudget(pixels),
|
|
803
|
-
testIdAttribute: this.runtime.config.testIdAttribute,
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Bytes one act observation may contribute to a model turn, clamped like a
|
|
808
|
-
* judgment's. The loop's history grows past what any one observation costs,
|
|
809
|
-
* so the fixed part counted here is what every turn resends: the project
|
|
810
|
-
* context and the prior-step ledger.
|
|
811
|
-
*/
|
|
812
|
-
observationByteBudget(pixels) {
|
|
813
|
-
const { config } = this.runtime;
|
|
814
|
-
return observationByteBudget({
|
|
815
|
-
maxObservationBytes: this.agent.config.maxObservationBytes,
|
|
816
|
-
maxModelTokensPerCall: config.limits.maxModelTokensPerCall,
|
|
817
|
-
}, { fixedBytes: this.metrics.contextBytes + this.metrics.ledgerBytes, pixels });
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Resolves an executor target against the newest observation. An id the
|
|
821
|
-
* newest observation no longer carries, but a recent one did, is re-found
|
|
822
|
-
* in the newest one by its descriptor: a turn that batches actions keeps
|
|
823
|
-
* addressing the screen it saw while each action's own look re-observes,
|
|
824
|
-
* and an engine that mints ids per observation renumbers the tree between
|
|
825
|
-
* them. Exactly one match, or the id counts as gone.
|
|
826
|
-
*/
|
|
827
|
-
resolveTarget(target) {
|
|
828
|
-
if (typeof target?.id !== 'string' || target.id === '') {
|
|
829
|
-
throw new TestError('INVALID_ARGUMENT', 'action target must be { id: string }');
|
|
830
|
-
}
|
|
831
|
-
const id = target.id.replace(/^#/, '');
|
|
832
|
-
const latest = this.latest;
|
|
833
|
-
if (latest === undefined) {
|
|
834
|
-
throw new AgentError('LOCATOR_NOT_FOUND', 'no observation has been captured yet; observe before acting');
|
|
835
|
-
}
|
|
836
|
-
const node = latest.nodes.get(id) ?? this.refound(id, latest);
|
|
837
|
-
if (node === undefined) {
|
|
838
|
-
throw new AgentError('LOCATOR_NOT_FOUND', `node #${id} is not on the current screen (observation ${latest.revision}); it was removed or never existed`);
|
|
839
|
-
}
|
|
840
|
-
return node;
|
|
841
|
-
}
|
|
842
|
-
/** The newest node matching the descriptor of what `id` named in a recent observation, if exactly one. */
|
|
843
|
-
refound(id, latest) {
|
|
844
|
-
let earlier;
|
|
845
|
-
for (let index = this.recent.length - 1; index >= 0 && earlier === undefined; index -= 1) {
|
|
846
|
-
earlier = this.recent[index].nodes.get(id);
|
|
847
|
-
}
|
|
848
|
-
if (earlier === undefined)
|
|
849
|
-
return undefined;
|
|
850
|
-
const descriptor = describeTarget(earlier, this.redact, this.runtime.config.testIdAttribute);
|
|
851
|
-
if (descriptor === undefined)
|
|
852
|
-
return undefined;
|
|
853
|
-
const relocated = relocateDescriptor(descriptor, latest.nodes, {
|
|
854
|
-
redact: this.redact,
|
|
855
|
-
testIdAttribute: this.runtime.config.testIdAttribute,
|
|
856
|
-
});
|
|
857
|
-
return relocated.kind === 'found' ? latest.nodes.get(relocated.id) : undefined;
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* Runs one grammar action against the action budget, recorded as an engine
|
|
861
|
-
* event. The body performs the engine call and returns the committed
|
|
862
|
-
* action's recordable descriptor — one value carries both concerns: the
|
|
863
|
-
* event's `detail` prose derives from it in a pure hook, and the dispatch
|
|
864
|
-
* writes it to the trace cache after the phase settles.
|
|
865
|
-
*/
|
|
866
|
-
runAction(name, body) {
|
|
867
|
-
return this.serialized(() => this.runActionNow(name, body));
|
|
868
|
-
}
|
|
869
|
-
async runActionNow(name, body) {
|
|
870
|
-
this.reserveAction();
|
|
871
|
-
let action;
|
|
872
|
-
try {
|
|
873
|
-
action = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'engine', phase: 'agent.action', name }, body, (committed) => ({
|
|
874
|
-
detail: describeAction(committed, this.redact, this.runtime.config.testIdAttribute).summary,
|
|
875
|
-
}));
|
|
876
|
-
}
|
|
877
|
-
catch (cause) {
|
|
878
|
-
this.checkpoint(cause);
|
|
879
|
-
throw cause;
|
|
880
|
-
}
|
|
881
|
-
// The effect may still be arriving: the next settled observation waits for
|
|
882
|
-
// the screen to leave the shape this action was resolved against. A secret
|
|
883
|
-
// fill leaves no visible trace and arms nothing; a scroll waits briefly for
|
|
884
|
-
// rows a windowed or lazy list renders.
|
|
885
|
-
if (name !== 'typeSecret')
|
|
886
|
-
this.armChange(name === 'scroll' ? BRIEF_CHANGE_WAIT_MS : undefined);
|
|
887
|
-
if (this.stepCache === undefined)
|
|
888
|
-
return;
|
|
889
|
-
// A typed value the step derived at run time is this run's data, not the
|
|
890
|
-
// flow's: it is recorded as a gap so replay hands over before it rather
|
|
891
|
-
// than typing a value the app may not issue again.
|
|
892
|
-
if (action.name === 'type' && isDerivedValue(action.value, this.spec.instruction, this.spec.params)) {
|
|
893
|
-
this.stepCache.recordGap('type (run-time value)');
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
this.stepCache.record(action);
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* One action against a resolved node. A node the engine reports stale is
|
|
900
|
-
* re-found by its descriptor against a fresh capture and the action retried
|
|
901
|
-
* (`MAX_STALE_RELOCATIONS` times): a list that remounts its rows between
|
|
902
|
-
* the observation and the action keeps the control on screen under a dead
|
|
903
|
-
* handle, and the control, not the handle, is what the model asked for. A
|
|
904
|
-
* descriptor that matches nothing or several nodes fails the action instead.
|
|
905
|
-
*/
|
|
906
|
-
commitTargeted(name, target, perform) {
|
|
907
|
-
return this.runAction(name, async () => {
|
|
908
|
-
let node = this.resolveTarget(target);
|
|
909
|
-
// resolveTarget guarantees an observation; it is the one the node came from.
|
|
910
|
-
let observation = this.latest;
|
|
911
|
-
for (let relocations = 0;; relocations += 1) {
|
|
912
|
-
// The container the node sits in is captured with it: that is what
|
|
913
|
-
// tells this row's "Delete" from the next row's when the flow replays.
|
|
914
|
-
const within = containerKey(node.ref.id, observation.nodes, observation.parents, this.redact);
|
|
915
|
-
// When the description still matches several controls, the position among
|
|
916
|
-
// them is recorded too; a replay that finds the same number picks the same one.
|
|
917
|
-
const position = describePosition(node, within, observation.nodes, {
|
|
918
|
-
redact: this.redact,
|
|
919
|
-
testIdAttribute: this.runtime.config.testIdAttribute,
|
|
920
|
-
});
|
|
921
|
-
try {
|
|
922
|
-
const action = await perform(node);
|
|
923
|
-
return {
|
|
924
|
-
...action,
|
|
925
|
-
...(within === undefined ? {} : { within }),
|
|
926
|
-
...(position === undefined ? {} : { position }),
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
catch (cause) {
|
|
930
|
-
if (asEngineError(cause)?.code !== 'NODE_STALE')
|
|
931
|
-
throw cause;
|
|
932
|
-
const relocated = relocations < MAX_STALE_RELOCATIONS ? await this.relocateStale(node) : undefined;
|
|
933
|
-
if (relocated === undefined) {
|
|
934
|
-
throw new AgentError('LOCATOR_NOT_FOUND', 'the target node left the screen before the action reached it', { cause });
|
|
935
|
-
}
|
|
936
|
-
node = relocated.node;
|
|
937
|
-
observation = relocated.observation;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Re-finds a node that went stale: one fresh capture, then the trace
|
|
944
|
-
* recorder's own descriptor matching (`cache/relocate.ts`) against it,
|
|
945
|
-
* exactly one match or nothing. The capture is taken directly rather than
|
|
946
|
-
* through the serialized observe: this runs inside a serialized action
|
|
947
|
-
* body, and a queued observation would wait on its own caller.
|
|
948
|
-
*/
|
|
949
|
-
async relocateStale(stale) {
|
|
950
|
-
const options = { redact: this.redact, testIdAttribute: this.runtime.config.testIdAttribute };
|
|
951
|
-
const descriptor = describeTarget(stale, this.redact, this.runtime.config.testIdAttribute);
|
|
952
|
-
if (descriptor === undefined)
|
|
953
|
-
return undefined;
|
|
954
|
-
this.checkpoint();
|
|
955
|
-
const observation = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'observation', phase: 'agent.observe', name: 'relocate' }, () => this.captureObservation(false), (prepared) => ({ count: prepared.nodes.size, bytes: prepared.bytes }));
|
|
956
|
-
this.publish(observation);
|
|
957
|
-
this.metrics.observationBytes = Math.max(this.metrics.observationBytes, observation.bytes);
|
|
958
|
-
const relocated = relocateDescriptor(descriptor, observation.nodes, options);
|
|
959
|
-
if (relocated.kind !== 'found')
|
|
960
|
-
return undefined;
|
|
961
|
-
const node = observation.nodes.get(relocated.id);
|
|
962
|
-
return node === undefined ? undefined : { node, observation };
|
|
963
|
-
}
|
|
964
|
-
async scroll(direction, target) {
|
|
965
|
-
if (!['up', 'down', 'left', 'right'].includes(direction)) {
|
|
966
|
-
throw new TestError('INVALID_ARGUMENT', `invalid scroll direction "${String(direction)}"`);
|
|
967
|
-
}
|
|
968
|
-
if (target === undefined) {
|
|
969
|
-
await this.runAction('scroll', async () => {
|
|
970
|
-
await this.session.swipe(direction, SCROLL_MOMENTUM, this.actionOperation());
|
|
971
|
-
return { name: 'scroll', direction };
|
|
972
|
-
});
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
await this.commitTargeted('scroll', target, async (node) => {
|
|
976
|
-
await this.session.perform(node.ref, { kind: 'swipe', direction, momentum: SCROLL_MOMENTUM }, this.actionOperation());
|
|
977
|
-
return { name: 'scroll', direction, node };
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* Fills one declared secret. The name must come from the step's own params
|
|
982
|
-
* — an executor can never fill a credential the test did not hand it — and
|
|
983
|
-
* the fill itself runs the full secret authorization policy: registered credential, origin allowlists, and an editable sink
|
|
984
|
-
* whose purpose matches. Pixel evidence is tainted from here on.
|
|
985
|
-
*/
|
|
986
|
-
async typeSecret(target, name) {
|
|
987
|
-
const secret = this.spec.secrets.get(name);
|
|
988
|
-
if (secret === undefined) {
|
|
989
|
-
throw new AgentError('POLICY_DENIED', `secret "${name}" was not declared in this step's params; only declared secrets can be filled`);
|
|
990
|
-
}
|
|
991
|
-
await this.commitTargeted('typeSecret', target, async (node) => {
|
|
992
|
-
const plaintext = await authorizeSecretFill({
|
|
993
|
-
session: this.session,
|
|
994
|
-
operation: () => this.actionOperation(),
|
|
995
|
-
recordPolicy: (policy, decision, code) => this.recordPolicy(policy, decision, code),
|
|
996
|
-
}, this.runtime, secret, node);
|
|
997
|
-
await this.session.perform(node.ref, { kind: 'fill', value: plaintext, sensitive: true }, this.actionOperation());
|
|
998
|
-
this.runtime.taint.value = true;
|
|
999
|
-
// Recorded by stable name only; replay re-runs the full authorization.
|
|
1000
|
-
return { name: 'typeSecret', node, secret: name };
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
recordPolicy(name, decision, code) {
|
|
1004
|
-
recordPolicyEvent(this.runtime.steps, name, decision, code);
|
|
1005
|
-
}
|
|
1006
|
-
async navigate(url) {
|
|
1007
|
-
if (typeof url !== 'string' || url.trim() === '') {
|
|
1008
|
-
throw new TestError('INVALID_ARGUMENT', 'navigate requires a URL');
|
|
1009
|
-
}
|
|
1010
|
-
const resolved = resolveNavigationUrl(url, this.runtime.app.base, this.runtime.app.allowedOrigins).url;
|
|
1011
|
-
// The raw argument is recorded, not the resolved URL: replay re-resolves
|
|
1012
|
-
// through the same base and origin policy this call just passed.
|
|
1013
|
-
await this.runAction('navigate', async () => {
|
|
1014
|
-
await this.session.app.open(resolved, this.operation());
|
|
1015
|
-
return { name: 'navigate', url };
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
406
|
+
}
|
|
407
|
+
/** The replay engine's view of a capture: the nodes, and the viewport a recorded point is checked against. */
|
|
408
|
+
function screenOf(observation) {
|
|
409
|
+
return { nodes: observation.nodes, viewport: { width: observation.viewport.width, height: observation.viewport.height } };
|
|
1018
410
|
}
|
|
1019
411
|
//# sourceMappingURL=act.js.map
|