@quiesco/agent 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -2
  3. package/metro.cjs +144 -0
  4. package/package.json +54 -3
  5. package/src/actions/hit-test.ts +42 -0
  6. package/src/actions/host.ts +194 -0
  7. package/src/actions/js.ts +245 -0
  8. package/src/actions/multitouch.ts +179 -0
  9. package/src/actions/native.ts +500 -0
  10. package/src/actions/performer.ts +87 -0
  11. package/src/actions/value.ts +460 -0
  12. package/src/actions/wheel.ts +260 -0
  13. package/src/api-types.ts +32 -0
  14. package/src/api.ts +11 -0
  15. package/src/assertions/index.ts +160 -0
  16. package/src/backdoor.ts +54 -0
  17. package/src/build-config.ts +6 -0
  18. package/src/connection/client.ts +196 -0
  19. package/src/connection/params.ts +63 -0
  20. package/src/early.ts +10 -0
  21. package/src/errors.ts +23 -0
  22. package/src/events.ts +136 -0
  23. package/src/expo-fetch.js +13 -0
  24. package/src/fiber-adapter/default-deps.native.ts +36 -0
  25. package/src/fiber-adapter/default-deps.ts +38 -0
  26. package/src/fiber-adapter/fiber.ts +81 -0
  27. package/src/fiber-adapter/hook.ts +154 -0
  28. package/src/fiber-adapter/host.ts +210 -0
  29. package/src/fiber-adapter/index.ts +221 -0
  30. package/src/fiber-adapter/scheduler.ts +70 -0
  31. package/src/fiber-adapter/traverse.ts +260 -0
  32. package/src/fiber-adapter/types.ts +142 -0
  33. package/src/index.ts +7 -0
  34. package/src/install.ts +6 -0
  35. package/src/json-check.ts +43 -0
  36. package/src/platform.ts +155 -0
  37. package/src/query/index.ts +5 -0
  38. package/src/query/matcher.ts +121 -0
  39. package/src/query/semantic.ts +266 -0
  40. package/src/query/tree.ts +116 -0
  41. package/src/query/visibility.ts +60 -0
  42. package/src/reset/index.ts +59 -0
  43. package/src/reset/storage-defaults.native.ts +26 -0
  44. package/src/reset/storage-defaults.ts +39 -0
  45. package/src/root/QuiescoRoot.tsx +28 -0
  46. package/src/runtime.ts +1003 -0
  47. package/src/singleton.ts +18 -0
  48. package/src/stub.ts +28 -0
  49. package/src/sync/frame-stable.ts +142 -0
  50. package/src/sync/idle-callback.ts +142 -0
  51. package/src/sync/index.ts +17 -0
  52. package/src/sync/native-settle.ts +41 -0
  53. package/src/sync/network.ts +299 -0
  54. package/src/sync/offline.ts +121 -0
  55. package/src/sync/react.ts +28 -0
  56. package/src/sync/timer.ts +124 -0
  57. package/src/sync/tracker.ts +51 -0
  58. package/src/sync/wait-for-idle.ts +141 -0
  59. package/src/version.ts +2 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Simulated offline mode (Phase 6.6 `app.device.setNetwork('offline')`; HU 2026-11-09: the agent blocks the
3
+ * app's connections on every platform, Android / the web also cut the device's network). While offline,
4
+ * the app's new fetch / XMLHttpRequest / WebSocket connections are sent to a host that never resolves
5
+ * (`.invalid`, RFC 6761), so they fail the way that platform fails without a network — React Native's
6
+ * "Network request failed", a browser's "Failed to fetch", a WebSocket error and close — instead of with
7
+ * made-up errors. Connections already open stay open. The agent's own connection is exempt.
8
+ *
9
+ * Installed before the network tracker, which wraps these patches and so still reports (and waits for)
10
+ * the URL the app asked for.
11
+ */
12
+ import { isFunction, patch } from './tracker';
13
+
14
+ /** Where blocked requests go: a name no resolver answers (RFC 6761 `.invalid`). */
15
+ export const OFFLINE_HTTP_URL = 'http://offline.quiesco.invalid/';
16
+ export const OFFLINE_WS_URL = 'ws://offline.quiesco.invalid/';
17
+
18
+ export type FetchFn = (this: unknown, input: unknown, init?: unknown) => Promise<unknown>;
19
+ type OpenFn = (this: unknown, method: string, url: string | URL, ...rest: unknown[]) => unknown;
20
+ type SocketCtor = new (url: string | URL, ...rest: unknown[]) => object;
21
+
22
+ export interface OfflineGlobals {
23
+ fetch?: FetchFn;
24
+ XMLHttpRequest?: { prototype: { open: OpenFn } };
25
+ WebSocket?: SocketCtor;
26
+ }
27
+
28
+ function urlOf(input: unknown): string {
29
+ if (typeof input === 'string') return input;
30
+ if (typeof input === 'object' && input !== null) {
31
+ const u = (input as { url?: unknown; href?: unknown }).url ?? (input as { href?: unknown }).href;
32
+ if (typeof u === 'string') return u;
33
+ }
34
+ return String(input);
35
+ }
36
+
37
+ /** `host:port` of an absolute URL (lower case, default ports left out as written), else null. */
38
+ export function hostOf(url: string): string | null {
39
+ const m = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?#]*@)?([^/?#]+)/i.exec(url);
40
+ return m === null ? null : (m[1] as string).toLowerCase();
41
+ }
42
+
43
+ /** Exempts the agent's own connection, `ws://localhost:<port>` (also reached as 127.0.0.1). */
44
+ export function ownConnection(port: number | string): (url: string) => boolean {
45
+ const own = new Set([`localhost:${port}`, `127.0.0.1:${port}`]);
46
+ return (url) => {
47
+ const h = hostOf(url);
48
+ return h !== null && own.has(h);
49
+ };
50
+ }
51
+
52
+ export class OfflineGate {
53
+ private offline = false;
54
+ private exempt: (url: string) => boolean = () => false;
55
+ private readonly restores: (() => void)[] = [];
56
+
57
+ constructor(private readonly target: OfflineGlobals = globalThis as unknown as OfflineGlobals) {}
58
+
59
+ get isOffline(): boolean {
60
+ return this.offline;
61
+ }
62
+
63
+ /** Applies to connections opened from now on. */
64
+ setOffline(offline: boolean): void {
65
+ this.offline = offline;
66
+ }
67
+
68
+ /** URLs that keep working offline (the agent's own connection). */
69
+ setExempt(exempt: (url: string) => boolean): void {
70
+ this.exempt = exempt;
71
+ }
72
+
73
+ blocks(url: string): boolean {
74
+ return this.offline && !this.exempt(url);
75
+ }
76
+
77
+ /** A fetch function that is blocked like the global one (used for `expo/fetch` imported directly). */
78
+ wrapFetch(original: FetchFn): FetchFn {
79
+ const self = this;
80
+ return function (this: unknown, input: unknown, init?: unknown) {
81
+ if (!self.blocks(urlOf(input))) return original.call(this, input, init);
82
+ // A Request object carries its method and signal; its body is irrelevant, the request fails.
83
+ const req = typeof input === 'object' && input !== null && typeof (input as { url?: unknown }).url === 'string' ? (input as { method?: unknown; signal?: unknown }) : null;
84
+ const merged = req === null ? init : { method: req.method, ...(req.signal !== undefined ? { signal: req.signal } : {}), ...(init as object | undefined) };
85
+ return original.call(this, OFFLINE_HTTP_URL, merged);
86
+ } as FetchFn;
87
+ }
88
+
89
+ install(): void {
90
+ if (this.restores.length > 0) return;
91
+ const self = this;
92
+ if (isFunction(this.target.fetch)) {
93
+ this.restores.push(patch(this.target, 'fetch', (original) => this.wrapFetch(original as FetchFn)));
94
+ }
95
+ const proto = this.target.XMLHttpRequest?.prototype;
96
+ if (proto !== undefined && isFunction(proto.open)) {
97
+ this.restores.push(
98
+ patch(proto, 'open', (original) =>
99
+ function (this: unknown, method: string, url: string | URL, ...rest: unknown[]) {
100
+ return original.call(this, method, self.blocks(urlOf(url)) ? OFFLINE_HTTP_URL : url, ...rest);
101
+ },
102
+ ),
103
+ );
104
+ }
105
+ const WS = this.target.WebSocket;
106
+ if (isFunction(WS)) {
107
+ // A subclass keeps `instanceof WebSocket`, the static constants and the app's event handlers.
108
+ const Offline = class extends WS {
109
+ constructor(url: string | URL, ...rest: unknown[]) {
110
+ super(self.blocks(urlOf(url)) ? OFFLINE_WS_URL : url, ...rest);
111
+ }
112
+ };
113
+ Object.defineProperty(Offline, 'name', { value: WS.name });
114
+ this.restores.push(patch(this.target, 'WebSocket', () => Offline as SocketCtor));
115
+ }
116
+ }
117
+
118
+ uninstall(): void {
119
+ while (this.restores.length > 0) this.restores.pop()?.();
120
+ }
121
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * React idle (requirements.md §6.2-3; docs/spikes/02-react-idle.md): no pending work on any FiberRoot
3
+ * (detector A) AND no unfinished scheduler task (detector B). The hook and scheduler patch are
4
+ * installed by the fiber adapter at agent load; this tracker only reads them.
5
+ */
6
+ import type { IdleTracker } from './tracker';
7
+
8
+ export interface ReactWorkSource {
9
+ pendingRootWork(): string[];
10
+ pendingSchedulerTasks(): string[];
11
+ }
12
+
13
+ export class ReactTracker implements IdleTracker {
14
+ readonly name = 'react';
15
+
16
+ constructor(private readonly source: ReactWorkSource) {}
17
+
18
+ isIdle(): boolean {
19
+ return this.source.pendingRootWork().length === 0 && this.source.pendingSchedulerTasks().length === 0;
20
+ }
21
+
22
+ describeBusy(): string[] {
23
+ return [...this.source.pendingRootWork(), ...this.source.pendingSchedulerTasks()];
24
+ }
25
+
26
+ install(): void {}
27
+ uninstall(): void {}
28
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pending timer tracker (requirements.md §6.2-2): counts `setTimeout` with delay ≤ threshold
3
+ * (default 1500ms). `setInterval` is only counted when `countIntervals` is set.
4
+ * The agent's own timers use `realClock` (captured before patching) and are never counted.
5
+ */
6
+ import { SyncDefaults } from '@quiesco/protocol';
7
+
8
+ import type { IdleTracker } from './tracker';
9
+ import { isFunction, patch } from './tracker';
10
+
11
+ type SetFn = (this: unknown, cb: unknown, ms?: number, ...args: unknown[]) => unknown;
12
+ type ClearFn = (this: unknown, id: unknown) => void;
13
+
14
+ export interface TimerGlobals {
15
+ setTimeout: SetFn;
16
+ clearTimeout: ClearFn;
17
+ setInterval?: SetFn;
18
+ clearInterval?: ClearFn;
19
+ }
20
+
21
+ export interface TimerTrackerOptions {
22
+ target?: TimerGlobals;
23
+ thresholdMs?: number;
24
+ countIntervals?: boolean;
25
+ now?: () => number;
26
+ }
27
+
28
+ interface PendingTimer {
29
+ kind: 'setTimeout' | 'setInterval';
30
+ delay: number;
31
+ since: number;
32
+ name: string;
33
+ }
34
+
35
+ export class TimerTracker implements IdleTracker {
36
+ readonly name = 'timer';
37
+ private readonly pending = new Map<unknown, PendingTimer>();
38
+ private readonly restores: (() => void)[] = [];
39
+ private readonly target: TimerGlobals;
40
+ private threshold: number;
41
+ private countIntervals: boolean;
42
+ private readonly now: () => number;
43
+
44
+ constructor(options: TimerTrackerOptions = {}) {
45
+ this.target = options.target ?? (globalThis as unknown as TimerGlobals);
46
+ this.threshold = options.thresholdMs ?? SyncDefaults.timerThresholdMs;
47
+ this.countIntervals = options.countIntervals ?? SyncDefaults.countIntervals;
48
+ this.now = options.now ?? Date.now;
49
+ }
50
+
51
+ /** Applies to timers created from now on. */
52
+ configure(options: { thresholdMs?: number; countIntervals?: boolean }): void {
53
+ if (options.thresholdMs !== undefined) this.threshold = options.thresholdMs;
54
+ if (options.countIntervals !== undefined) {
55
+ this.countIntervals = options.countIntervals;
56
+ if (!this.countIntervals) for (const [id, p] of this.pending) if (p.kind === 'setInterval') this.pending.delete(id);
57
+ }
58
+ }
59
+
60
+ isIdle(): boolean {
61
+ return this.pending.size === 0;
62
+ }
63
+
64
+ describeBusy(): string[] {
65
+ const t = this.now();
66
+ return [...this.pending.values()].map((p) => {
67
+ const left = p.kind === 'setTimeout' ? `, fires in ${Math.max(0, p.delay - (t - p.since))}ms` : '';
68
+ return `${p.kind}(${p.name}, ${p.delay}ms)${left}`;
69
+ });
70
+ }
71
+
72
+ install(): void {
73
+ if (this.restores.length > 0) return;
74
+ const self = this;
75
+ const t = this.target;
76
+ this.restores.push(
77
+ patch(t, 'setTimeout', (original) =>
78
+ function (this: unknown, cb: unknown, ms?: number, ...args: unknown[]) {
79
+ const delay = Number(ms ?? 0) || 0;
80
+ if (!isFunction(cb) || delay > self.threshold) return original.call(this, cb, ms, ...args);
81
+ const wrapped = function (this: unknown, ...a: unknown[]) {
82
+ self.pending.delete(id);
83
+ return (cb as (...x: unknown[]) => unknown).apply(this, a);
84
+ };
85
+ const id: unknown = original.call(this, wrapped, ms, ...args);
86
+ self.pending.set(id, { kind: 'setTimeout', delay, since: self.now(), name: cb.name || 'anonymous' });
87
+ return id;
88
+ },
89
+ ),
90
+ patch(t, 'clearTimeout', (original) =>
91
+ function (this: unknown, id: unknown) {
92
+ self.pending.delete(id);
93
+ return original.call(this, id);
94
+ },
95
+ ),
96
+ );
97
+ if (isFunction(t.setInterval) && isFunction(t.clearInterval)) {
98
+ const it = t as Required<TimerGlobals>;
99
+ this.restores.push(
100
+ patch(it, 'setInterval', (original) =>
101
+ function (this: unknown, cb: unknown, ms?: number, ...args: unknown[]) {
102
+ const delay = Number(ms ?? 0) || 0;
103
+ const id = original.call(this, cb, ms, ...args);
104
+ if (self.countIntervals && isFunction(cb) && delay <= self.threshold) {
105
+ self.pending.set(id, { kind: 'setInterval', delay, since: self.now(), name: cb.name || 'anonymous' });
106
+ }
107
+ return id;
108
+ },
109
+ ),
110
+ patch(it, 'clearInterval', (original) =>
111
+ function (this: unknown, id: unknown) {
112
+ self.pending.delete(id);
113
+ return original.call(this, id);
114
+ },
115
+ ),
116
+ );
117
+ }
118
+ }
119
+
120
+ uninstall(): void {
121
+ while (this.restores.length > 0) this.restores.pop()?.();
122
+ this.pending.clear();
123
+ }
124
+ }
@@ -0,0 +1,51 @@
1
+ /** Idle tracker contract (requirements.md §6.2). */
2
+ export interface IdleTracker {
3
+ readonly name: string;
4
+ isIdle(): boolean;
5
+ /** Human-readable reasons, e.g. `GET https://api.example.com/todos (1200ms)`. Empty when idle. */
6
+ describeBusy(): string[];
7
+ install(): void;
8
+ uninstall(): void;
9
+ /** Optional: called when a waitForIdle starts / ends (frame-stable only samples while waiting). */
10
+ beginWait?(): void;
11
+ endWait?(): void;
12
+ }
13
+
14
+ /** Timer functions the agent uses for its own scheduling. They must NOT be the tracked ones. */
15
+ export interface Clock {
16
+ setTimeout(cb: () => void, ms: number): unknown;
17
+ clearTimeout(id: unknown): void;
18
+ now(): number;
19
+ }
20
+
21
+ type AnyFn = (...args: never[]) => unknown;
22
+
23
+ /** Replace `obj[key]` and return a function that restores it (only if nobody replaced it since). */
24
+ export function patch<T extends object, K extends keyof T>(obj: T, key: K, make: (original: T[K]) => T[K]): () => void {
25
+ const original = obj[key];
26
+ const replacement = make(original);
27
+ obj[key] = replacement;
28
+ return () => {
29
+ if (obj[key] === replacement) obj[key] = original;
30
+ };
31
+ }
32
+
33
+ export function isFunction(v: unknown): v is AnyFn {
34
+ return typeof v === 'function';
35
+ }
36
+
37
+ /**
38
+ * Real timers captured when this module is first evaluated, i.e. before any tracker patches the
39
+ * globals (the agent is the first import of the app entry).
40
+ */
41
+ const g = globalThis as unknown as {
42
+ setTimeout: (cb: () => void, ms: number) => unknown;
43
+ clearTimeout: (id: unknown) => void;
44
+ };
45
+ const realSetTimeout = g.setTimeout;
46
+ const realClearTimeout = g.clearTimeout;
47
+ export const realClock: Clock = {
48
+ setTimeout: (cb, ms) => realSetTimeout.call(globalThis, cb, ms),
49
+ clearTimeout: (id) => realClearTimeout.call(globalThis, id),
50
+ now: () => Date.now(),
51
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * waitForIdle (requirements.md §6.2): all enabled trackers idle in two consecutive checks that run
3
+ * in DIFFERENT macrotasks (docs/spikes/02-react-idle.md §4 — a single check misreports idle when a
4
+ * batch of expired timers runs without draining microtasks in between).
5
+ */
6
+ import { SyncDefaults } from '@quiesco/protocol';
7
+ import type { TrackerState } from '@quiesco/protocol';
8
+
9
+ import { QuiescoError } from '../errors';
10
+ import type { Clock, IdleTracker } from './tracker';
11
+ import { realClock } from './tracker';
12
+
13
+ export interface WaitForIdleOptions {
14
+ timeoutMs?: number;
15
+ /** Delay between checks while busy. The confirmation check always runs in the very next macrotask. */
16
+ pollIntervalMs?: number;
17
+ /**
18
+ * Timeout of the whole call this wait belongs to (an action waits with what is left of it), used in
19
+ * the SYNC_TIMEOUT message. Default: `timeoutMs`.
20
+ */
21
+ reportedTimeoutMs?: number;
22
+ }
23
+
24
+ const DEFAULT_POLL_MS = 16;
25
+ const NO_NAMES: ReadonlySet<string> = new Set();
26
+
27
+ export class SyncEngine {
28
+ private installed = false;
29
+ private disabled = new Set<string>();
30
+ /** Trackers that cannot settle right now (e.g. no frames while the app is in the background). */
31
+ private skipped: () => ReadonlySet<string> = () => NO_NAMES;
32
+ private inBackground: () => boolean = () => false;
33
+ private onBackgroundChange: ((wake: () => void) => () => void) | undefined;
34
+
35
+ constructor(
36
+ readonly trackers: readonly IdleTracker[],
37
+ private readonly clock: Clock = realClock,
38
+ ) {}
39
+
40
+ install(): void {
41
+ if (this.installed) return;
42
+ for (const t of this.trackers) t.install();
43
+ this.installed = true;
44
+ }
45
+
46
+ uninstall(): void {
47
+ if (!this.installed) return;
48
+ for (const t of this.trackers) t.uninstall();
49
+ this.installed = false;
50
+ }
51
+
52
+ /** Trackers listed as `false` are ignored by waitForIdle / getIdleState (they stay installed). */
53
+ setEnabled(enabled: Readonly<Record<string, boolean | undefined>>): void {
54
+ this.disabled = new Set(Object.entries(enabled).filter(([, on]) => on === false).map(([name]) => name));
55
+ }
56
+
57
+ /**
58
+ * Trackers named by `fn()` at check time count as idle (Phase 6.5: in the background Android pauses
59
+ * RN timers and no frames are drawn, so they would never settle).
60
+ */
61
+ setSkipped(fn: () => ReadonlySet<string>): void {
62
+ this.skipped = fn;
63
+ }
64
+
65
+ /**
66
+ * While `inBackground()`, Android pauses the JS timers (findings 6.7 batch C: a permission dialog is
67
+ * another activity), so a wait sleeping on a timer would neither end nor time out. `onChange` wakes it
68
+ * (AppState events still arrive), and in the background it returns as soon as every active tracker is
69
+ * idle, without the confirmation check in a later macrotask (that needs a timer; nothing runs anyway).
70
+ */
71
+ setBackground(inBackground: () => boolean, onChange?: (wake: () => void) => () => void): void {
72
+ this.inBackground = inBackground;
73
+ this.onBackgroundChange = onChange;
74
+ }
75
+
76
+ private active(): IdleTracker[] {
77
+ const skipped = safe(() => this.skipped(), NO_NAMES);
78
+ return this.trackers.filter((t) => !this.disabled.has(t.name) && !skipped.has(t.name));
79
+ }
80
+
81
+ getIdleState(): TrackerState[] {
82
+ return this.active().map((t) => {
83
+ const idle = safe(() => t.isIdle(), false);
84
+ return { name: t.name, idle, busy: idle ? [] : safe(() => t.describeBusy(), ['describeBusy() threw']) };
85
+ });
86
+ }
87
+
88
+ async waitForIdle(options: WaitForIdleOptions = {}): Promise<{ waitedMs: number }> {
89
+ const timeoutMs = options.timeoutMs ?? SyncDefaults.timeoutMs;
90
+ const pollMs = options.pollIntervalMs ?? DEFAULT_POLL_MS;
91
+ const start = this.clock.now();
92
+ const trackers = this.active();
93
+ let wake: (() => void) | null = null;
94
+ const unsubscribe = safe(() => this.onBackgroundChange?.(() => wake?.()), undefined);
95
+ const sleep = (ms: number) =>
96
+ new Promise<void>((resolve) => {
97
+ wake = resolve;
98
+ this.clock.setTimeout(resolve, ms);
99
+ });
100
+ for (const t of trackers) t.beginWait?.();
101
+ try {
102
+ for (;;) {
103
+ if (this.allIdle()) {
104
+ if (safe(() => this.inBackground(), false)) return { waitedMs: this.clock.now() - start };
105
+ await sleep(0);
106
+ if (this.allIdle()) return { waitedMs: this.clock.now() - start };
107
+ }
108
+ const elapsed = this.clock.now() - start;
109
+ if (elapsed >= timeoutMs) throw this.timeoutError(options.reportedTimeoutMs ?? timeoutMs, elapsed);
110
+ await sleep(Math.min(pollMs, timeoutMs - elapsed));
111
+ }
112
+ } finally {
113
+ wake = null;
114
+ safe(() => unsubscribe?.(), undefined);
115
+ for (const t of trackers) t.endWait?.();
116
+ }
117
+ }
118
+
119
+ private allIdle(): boolean {
120
+ return this.active().every((t) => safe(() => t.isIdle(), false));
121
+ }
122
+
123
+ private timeoutError(timeoutMs: number, waitedMs: number): QuiescoError<'SYNC_TIMEOUT'> {
124
+ const busy = this.getIdleState().filter((s) => !s.idle);
125
+ const lines = busy.map((s) => ` - ${s.name}: ${s.busy.length > 0 ? s.busy.join('; ') : 'busy'}`);
126
+ const waited = waitedMs < timeoutMs ? ` (waited ${waitedMs}ms for idle after the action)` : '';
127
+ return new QuiescoError('SYNC_TIMEOUT', `App did not become idle within the ${timeoutMs}ms timeout${waited}. Busy:\n${lines.join('\n')}`, {
128
+ timeoutMs,
129
+ busy,
130
+ });
131
+ }
132
+
133
+ }
134
+
135
+ function safe<T>(fn: () => T, fallback: T): T {
136
+ try {
137
+ return fn();
138
+ } catch {
139
+ return fallback;
140
+ }
141
+ }
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Reported in `hello`. Keep in sync with package.json (checked by a unit test). */
2
+ export const AGENT_VERSION = '0.1.0';