screenhand 0.2.0 → 0.3.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.
Files changed (212) hide show
  1. package/README.md +165 -446
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +3615 -400
  4. package/dist/scripts/export-help-center.js +112 -0
  5. package/dist/scripts/marketing-loop.js +117 -0
  6. package/dist/scripts/observer-daemon.js +288 -0
  7. package/dist/scripts/orchestrator-daemon.js +399 -0
  8. package/dist/scripts/threads-campaign.js +208 -0
  9. package/dist/src/community/fetcher.js +109 -0
  10. package/dist/src/community/index.js +6 -0
  11. package/dist/src/community/publisher.js +191 -0
  12. package/dist/src/community/remote-api.js +121 -0
  13. package/dist/src/community/types.js +3 -0
  14. package/dist/src/community/validator.js +95 -0
  15. package/dist/src/context-tracker.js +489 -0
  16. package/dist/src/ingestion/coverage-auditor.js +233 -0
  17. package/dist/src/ingestion/doc-parser.js +164 -0
  18. package/dist/src/ingestion/index.js +8 -0
  19. package/dist/src/ingestion/menu-scanner.js +152 -0
  20. package/dist/src/ingestion/reference-merger.js +186 -0
  21. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  22. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  23. package/dist/src/ingestion/types.js +3 -0
  24. package/dist/src/jobs/manager.js +82 -14
  25. package/dist/src/jobs/runner.js +138 -15
  26. package/dist/src/learning/engine.js +356 -0
  27. package/dist/src/learning/index.js +9 -0
  28. package/dist/src/learning/locator-policy.js +120 -0
  29. package/dist/src/learning/pattern-policy.js +89 -0
  30. package/dist/src/learning/recovery-policy.js +116 -0
  31. package/dist/src/learning/sensor-policy.js +115 -0
  32. package/dist/src/learning/timing-model.js +204 -0
  33. package/dist/src/learning/topology-policy.js +90 -0
  34. package/dist/src/learning/types.js +9 -0
  35. package/dist/src/logging/timeline-logger.js +4 -1
  36. package/dist/src/memory/playbook-seeds.js +200 -0
  37. package/dist/src/memory/recall.js +60 -8
  38. package/dist/src/memory/service.js +30 -5
  39. package/dist/src/memory/store.js +34 -5
  40. package/dist/src/native/bridge-client.js +253 -31
  41. package/dist/src/observer/state.js +199 -0
  42. package/dist/src/observer/types.js +43 -0
  43. package/dist/src/orchestrator/state.js +68 -0
  44. package/dist/src/orchestrator/types.js +22 -0
  45. package/dist/src/perception/ax-source.js +162 -0
  46. package/dist/src/perception/cdp-source.js +162 -0
  47. package/dist/src/perception/coordinator.js +771 -0
  48. package/dist/src/perception/frame-differ.js +287 -0
  49. package/dist/src/perception/index.js +22 -0
  50. package/dist/src/perception/manager.js +199 -0
  51. package/dist/src/perception/types.js +47 -0
  52. package/dist/src/perception/vision-source.js +399 -0
  53. package/dist/src/planner/deterministic.js +298 -0
  54. package/dist/src/planner/executor.js +870 -0
  55. package/dist/src/planner/goal-store.js +92 -0
  56. package/dist/src/planner/index.js +21 -0
  57. package/dist/src/planner/planner.js +520 -0
  58. package/dist/src/planner/tool-registry.js +71 -0
  59. package/dist/src/planner/types.js +22 -0
  60. package/dist/src/platform/explorer.js +213 -0
  61. package/dist/src/platform/help-center-markdown.js +527 -0
  62. package/dist/src/platform/learner.js +257 -0
  63. package/dist/src/playbook/engine.js +296 -11
  64. package/dist/src/playbook/mcp-recorder.js +204 -0
  65. package/dist/src/playbook/recorder.js +3 -2
  66. package/dist/src/playbook/runner.js +1 -1
  67. package/dist/src/playbook/store.js +139 -10
  68. package/dist/src/recovery/detectors.js +156 -0
  69. package/dist/src/recovery/engine.js +327 -0
  70. package/dist/src/recovery/index.js +20 -0
  71. package/dist/src/recovery/strategies.js +274 -0
  72. package/dist/src/recovery/types.js +20 -0
  73. package/dist/src/runtime/accessibility-adapter.js +55 -18
  74. package/dist/src/runtime/applescript-adapter.js +8 -2
  75. package/dist/src/runtime/cdp-chrome-adapter.js +1 -1
  76. package/dist/src/runtime/executor.js +23 -3
  77. package/dist/src/runtime/locator-cache.js +24 -2
  78. package/dist/src/runtime/service.js +59 -15
  79. package/dist/src/runtime/session-manager.js +4 -1
  80. package/dist/src/runtime/vision-adapter.js +2 -1
  81. package/dist/src/state/app-map-types.js +72 -0
  82. package/dist/src/state/app-map.js +1974 -0
  83. package/dist/src/state/entity-tracker.js +108 -0
  84. package/dist/src/state/fusion.js +96 -0
  85. package/dist/src/state/index.js +21 -0
  86. package/dist/src/state/ladder-generator.js +236 -0
  87. package/dist/src/state/persistence.js +156 -0
  88. package/dist/src/state/types.js +17 -0
  89. package/dist/src/state/world-model.js +1456 -0
  90. package/dist/src/util/atomic-write.js +19 -4
  91. package/dist/src/util/sanitize.js +146 -0
  92. package/dist-app-maps/com.figma.Desktop.json +959 -0
  93. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  94. package/dist-app-maps/notion.id.json +2831 -0
  95. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  96. package/dist-playbooks/codex-desktop.json +76 -0
  97. package/dist-playbooks/competitor-research-stack.json +122 -0
  98. package/dist-playbooks/davinci-color-grade.json +153 -0
  99. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  100. package/dist-playbooks/davinci-render.json +114 -0
  101. package/dist-playbooks/devto.json +52 -0
  102. package/dist-playbooks/discord.json +41 -0
  103. package/dist-playbooks/google-flow-create-project.json +59 -0
  104. package/dist-playbooks/google-flow-edit-image.json +90 -0
  105. package/dist-playbooks/google-flow-edit-video.json +90 -0
  106. package/dist-playbooks/google-flow-generate-image.json +68 -0
  107. package/dist-playbooks/google-flow-generate-video.json +191 -0
  108. package/dist-playbooks/google-flow-open-project.json +48 -0
  109. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  110. package/dist-playbooks/google-flow-search-assets.json +64 -0
  111. package/dist-playbooks/instagram.json +57 -0
  112. package/dist-playbooks/linkedin.json +52 -0
  113. package/dist-playbooks/n8n.json +43 -0
  114. package/dist-playbooks/reddit.json +52 -0
  115. package/dist-playbooks/threads.json +59 -0
  116. package/dist-playbooks/x-twitter.json +59 -0
  117. package/dist-playbooks/youtube.json +59 -0
  118. package/dist-references/canva.json +646 -0
  119. package/dist-references/codex-desktop.json +305 -0
  120. package/dist-references/davinci-resolve-keyboard.json +594 -0
  121. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  122. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  123. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  124. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  125. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  126. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  127. package/dist-references/devpost.json +186 -0
  128. package/dist-references/devto.json +317 -0
  129. package/dist-references/discord.json +549 -0
  130. package/dist-references/figma.json +1186 -0
  131. package/dist-references/finder.json +146 -0
  132. package/dist-references/google-ads-transparency.json +95 -0
  133. package/dist-references/google-flow.json +649 -0
  134. package/dist-references/instagram.json +341 -0
  135. package/dist-references/linkedin.json +324 -0
  136. package/dist-references/meta-ad-library.json +86 -0
  137. package/dist-references/n8n.json +387 -0
  138. package/dist-references/notes.json +27 -0
  139. package/dist-references/notion.json +163 -0
  140. package/dist-references/reddit.json +341 -0
  141. package/dist-references/threads.json +337 -0
  142. package/dist-references/x-twitter.json +403 -0
  143. package/dist-references/youtube.json +373 -0
  144. package/native/macos-bridge/Package.swift +22 -0
  145. package/native/macos-bridge/Sources/AccessibilityBridge.swift +482 -0
  146. package/native/macos-bridge/Sources/AppManagement.swift +339 -0
  147. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +537 -0
  148. package/native/macos-bridge/Sources/ObserverBridge.swift +120 -0
  149. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  150. package/native/macos-bridge/Sources/VisionBridge.swift +238 -0
  151. package/native/macos-bridge/Sources/main.swift +498 -0
  152. package/native/windows-bridge/AppManagement.cs +234 -0
  153. package/native/windows-bridge/InputBridge.cs +436 -0
  154. package/native/windows-bridge/Program.cs +270 -0
  155. package/native/windows-bridge/ScreenCapture.cs +453 -0
  156. package/native/windows-bridge/UIAutomationBridge.cs +571 -0
  157. package/native/windows-bridge/WindowsBridge.csproj +17 -0
  158. package/package.json +12 -1
  159. package/scripts/postinstall.cjs +127 -0
  160. package/dist/.audit-log.jsonl +0 -55
  161. package/dist/.screenhand/memory/.lock +0 -1
  162. package/dist/.screenhand/memory/actions.jsonl +0 -85
  163. package/dist/.screenhand/memory/errors.jsonl +0 -5
  164. package/dist/.screenhand/memory/errors.jsonl.bak +0 -4
  165. package/dist/.screenhand/memory/state.json +0 -35
  166. package/dist/.screenhand/memory/state.json.bak +0 -35
  167. package/dist/.screenhand/memory/strategies.jsonl +0 -12
  168. package/dist/agent/cli.js +0 -73
  169. package/dist/agent/loop.js +0 -258
  170. package/dist/config.js +0 -9
  171. package/dist/index.js +0 -56
  172. package/dist/logging/timeline-logger.js +0 -29
  173. package/dist/mcp/mcp-stdio-server.js +0 -448
  174. package/dist/mcp/server.js +0 -347
  175. package/dist/mcp-entry.js +0 -59
  176. package/dist/memory/recall.js +0 -160
  177. package/dist/memory/research.js +0 -98
  178. package/dist/memory/seeds.js +0 -89
  179. package/dist/memory/session.js +0 -161
  180. package/dist/memory/store.js +0 -391
  181. package/dist/memory/types.js +0 -4
  182. package/dist/monitor/codex-monitor.js +0 -377
  183. package/dist/monitor/task-queue.js +0 -84
  184. package/dist/monitor/types.js +0 -49
  185. package/dist/native/bridge-client.js +0 -174
  186. package/dist/native/macos-bridge-client.js +0 -5
  187. package/dist/npm-publish-helper.js +0 -117
  188. package/dist/npm-token-cdp.js +0 -113
  189. package/dist/npm-token-create.js +0 -135
  190. package/dist/npm-token-finish.js +0 -126
  191. package/dist/playbook/engine.js +0 -193
  192. package/dist/playbook/index.js +0 -4
  193. package/dist/playbook/recorder.js +0 -519
  194. package/dist/playbook/runner.js +0 -392
  195. package/dist/playbook/store.js +0 -166
  196. package/dist/playbook/types.js +0 -4
  197. package/dist/runtime/accessibility-adapter.js +0 -377
  198. package/dist/runtime/app-adapter.js +0 -48
  199. package/dist/runtime/applescript-adapter.js +0 -283
  200. package/dist/runtime/ax-role-map.js +0 -80
  201. package/dist/runtime/browser-adapter.js +0 -36
  202. package/dist/runtime/cdp-chrome-adapter.js +0 -505
  203. package/dist/runtime/composite-adapter.js +0 -205
  204. package/dist/runtime/executor.js +0 -250
  205. package/dist/runtime/locator-cache.js +0 -12
  206. package/dist/runtime/planning-loop.js +0 -47
  207. package/dist/runtime/service.js +0 -372
  208. package/dist/runtime/session-manager.js +0 -28
  209. package/dist/runtime/state-observer.js +0 -105
  210. package/dist/runtime/vision-adapter.js +0 -208
  211. package/dist/test-mcp-protocol.js +0 -138
  212. package/dist/types.js +0 -1
@@ -0,0 +1,120 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * LocatorPolicy — tracks per-app×action locator reliability.
5
+ *
6
+ * After enough data points, recommends the highest-scoring locator
7
+ * for a given action in a given app. Uses Bayesian scoring so
8
+ * a locator with 3/3 successes doesn't beat one with 50/55.
9
+ */
10
+ export class LocatorPolicy {
11
+ /** Map<compoundKey, LocatorEntry[]> — multiple locators per key */
12
+ entries = new Map();
13
+ priorStrength;
14
+ constructor(priorStrength = 2) {
15
+ this.priorStrength = priorStrength;
16
+ }
17
+ /**
18
+ * Record a locator outcome and update the score.
19
+ */
20
+ static makeKey(bundleId, actionKey) {
21
+ return `${bundleId.length}:${bundleId}\0${actionKey}`;
22
+ }
23
+ record(outcome) {
24
+ const compoundKey = LocatorPolicy.makeKey(outcome.bundleId, outcome.actionKey);
25
+ let list = this.entries.get(compoundKey);
26
+ if (!list) {
27
+ list = [];
28
+ this.entries.set(compoundKey, list);
29
+ }
30
+ let entry = list.find((e) => e.locator === outcome.locator && e.method === outcome.method);
31
+ if (!entry) {
32
+ entry = {
33
+ key: compoundKey,
34
+ locator: outcome.locator,
35
+ method: outcome.method,
36
+ successCount: 0,
37
+ failCount: 0,
38
+ score: 0.5,
39
+ lastUsed: new Date().toISOString(),
40
+ };
41
+ list.push(entry);
42
+ }
43
+ if (outcome.success) {
44
+ entry.successCount++;
45
+ }
46
+ else {
47
+ entry.failCount++;
48
+ }
49
+ entry.score = this.bayesianScore(entry.successCount, entry.failCount);
50
+ entry.lastUsed = new Date().toISOString();
51
+ }
52
+ /**
53
+ * Get the best locator for a given app×action.
54
+ * Returns null if no data or insufficient samples.
55
+ */
56
+ recommend(bundleId, actionKey, minSamples = 5) {
57
+ const compoundKey = LocatorPolicy.makeKey(bundleId, actionKey);
58
+ const list = this.entries.get(compoundKey);
59
+ if (!list || list.length === 0)
60
+ return null;
61
+ const qualified = list.filter((e) => e.successCount + e.failCount >= minSamples);
62
+ if (qualified.length === 0)
63
+ return null;
64
+ qualified.sort((a, b) => b.score - a.score);
65
+ return qualified[0];
66
+ }
67
+ /**
68
+ * Get all entries for a given app×action (for inspection/debugging).
69
+ */
70
+ getEntries(bundleId, actionKey) {
71
+ const compoundKey = LocatorPolicy.makeKey(bundleId, actionKey);
72
+ return this.entries.get(compoundKey) ?? [];
73
+ }
74
+ /**
75
+ * Clear all entries.
76
+ */
77
+ clear() {
78
+ this.entries.clear();
79
+ }
80
+ /**
81
+ * Get all entries across all keys (for persistence).
82
+ */
83
+ getAllEntries() {
84
+ const result = [];
85
+ for (const list of this.entries.values()) {
86
+ result.push(...list);
87
+ }
88
+ return result;
89
+ }
90
+ /**
91
+ * Load entries from persisted data.
92
+ */
93
+ loadEntries(entries) {
94
+ for (const entry of entries) {
95
+ let list = this.entries.get(entry.key);
96
+ if (!list) {
97
+ list = [];
98
+ this.entries.set(entry.key, list);
99
+ }
100
+ const existing = list.find((e) => e.locator === entry.locator && e.method === entry.method);
101
+ if (existing) {
102
+ existing.successCount = entry.successCount;
103
+ existing.failCount = entry.failCount;
104
+ existing.score = entry.score;
105
+ existing.lastUsed = entry.lastUsed;
106
+ }
107
+ else {
108
+ list.push({ ...entry });
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Bayesian score: (successes + prior) / (total + 2*prior)
114
+ * With prior=2: starts at 0.5, converges to true rate with more data.
115
+ */
116
+ bayesianScore(successes, failures) {
117
+ return ((successes + this.priorStrength) /
118
+ (successes + failures + 2 * this.priorStrength));
119
+ }
120
+ }
@@ -0,0 +1,89 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * PatternPolicy — learns which tool+locator combos work for each app.
5
+ *
6
+ * Persisted to `patterns.jsonl`. Each entry tracks success/fail counts
7
+ * for a specific bundleId×tool×locator triple, scored with Bayesian averaging.
8
+ *
9
+ * Used by the intelligence wrapper to recommend known-good selectors
10
+ * and warn about known-bad ones.
11
+ */
12
+ export class PatternPolicy {
13
+ entries = new Map();
14
+ priorStrength;
15
+ constructor(priorStrength = 2) {
16
+ this.priorStrength = priorStrength;
17
+ }
18
+ /**
19
+ * Record a pattern outcome (tool+locator success/failure for an app).
20
+ */
21
+ record(outcome) {
22
+ const key = `${outcome.bundleId}::${outcome.tool}::${outcome.locator}`;
23
+ let entry = this.entries.get(key);
24
+ if (!entry) {
25
+ entry = {
26
+ key,
27
+ bundleId: outcome.bundleId,
28
+ tool: outcome.tool,
29
+ locator: outcome.locator,
30
+ method: outcome.method,
31
+ successCount: 0,
32
+ failCount: 0,
33
+ score: 0.5,
34
+ lastSeen: new Date().toISOString(),
35
+ };
36
+ this.entries.set(key, entry);
37
+ }
38
+ if (outcome.success) {
39
+ entry.successCount++;
40
+ }
41
+ else {
42
+ entry.failCount++;
43
+ }
44
+ entry.score = this.bayesianScore(entry.successCount, entry.failCount);
45
+ entry.lastSeen = new Date().toISOString();
46
+ }
47
+ /**
48
+ * Query patterns for a given app, optionally filtered by tool.
49
+ * Returns entries sorted by score descending.
50
+ */
51
+ query(bundleId, tool) {
52
+ const results = [];
53
+ for (const entry of this.entries.values()) {
54
+ if (entry.bundleId !== bundleId)
55
+ continue;
56
+ if (tool && entry.tool !== tool)
57
+ continue;
58
+ results.push(entry);
59
+ }
60
+ return results.sort((a, b) => b.score - a.score);
61
+ }
62
+ /**
63
+ * Get the best pattern for a given app×tool, or null if insufficient data.
64
+ */
65
+ recommend(bundleId, tool, minSamples = 3) {
66
+ const candidates = this.query(bundleId, tool);
67
+ for (const entry of candidates) {
68
+ if (entry.successCount + entry.failCount >= minSamples && entry.score > 0.5) {
69
+ return entry;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ clear() {
75
+ this.entries.clear();
76
+ }
77
+ getAllEntries() {
78
+ return [...this.entries.values()];
79
+ }
80
+ loadEntries(entries) {
81
+ for (const entry of entries) {
82
+ this.entries.set(entry.key, { ...entry });
83
+ }
84
+ }
85
+ bayesianScore(successes, failures) {
86
+ return ((successes + this.priorStrength) /
87
+ (successes + failures + 2 * this.priorStrength));
88
+ }
89
+ }
@@ -0,0 +1,116 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * RecoveryPolicy — ranks recovery strategies per blocker×app.
5
+ *
6
+ * When the RecoveryEngine needs to pick a strategy, this policy
7
+ * provides a ranked list based on past success rates. Over time,
8
+ * the best strategy surfaces to the top.
9
+ */
10
+ export class RecoveryPolicy {
11
+ /** Map<compoundKey, RecoveryPolicyEntry[]> */
12
+ entries = new Map();
13
+ priorStrength;
14
+ constructor(priorStrength = 2) {
15
+ this.priorStrength = priorStrength;
16
+ }
17
+ /**
18
+ * Record a recovery outcome.
19
+ */
20
+ record(outcome) {
21
+ const compoundKey = `${outcome.blockerType}::${outcome.bundleId}`;
22
+ let list = this.entries.get(compoundKey);
23
+ if (!list) {
24
+ list = [];
25
+ this.entries.set(compoundKey, list);
26
+ }
27
+ let entry = list.find((e) => e.strategyId === outcome.strategyId);
28
+ if (!entry) {
29
+ entry = {
30
+ key: compoundKey,
31
+ strategyId: outcome.strategyId,
32
+ successCount: 0,
33
+ failCount: 0,
34
+ score: 0.5,
35
+ avgDurationMs: 0,
36
+ lastUsed: new Date().toISOString(),
37
+ };
38
+ list.push(entry);
39
+ }
40
+ if (outcome.success) {
41
+ entry.successCount++;
42
+ }
43
+ else {
44
+ entry.failCount++;
45
+ }
46
+ // Running average for duration — guard against NaN
47
+ const duration = Number.isFinite(outcome.durationMs) ? outcome.durationMs : 0;
48
+ const total = entry.successCount + entry.failCount;
49
+ entry.avgDurationMs =
50
+ entry.avgDurationMs * ((total - 1) / total) +
51
+ duration / total;
52
+ entry.score = this.bayesianScore(entry.successCount, entry.failCount);
53
+ entry.lastUsed = new Date().toISOString();
54
+ }
55
+ /**
56
+ * Rank strategies for a given blocker×app, best first.
57
+ * Returns strategy IDs sorted by score (descending).
58
+ */
59
+ rank(blockerType, bundleId) {
60
+ const compoundKey = `${blockerType}::${bundleId}`;
61
+ const list = this.entries.get(compoundKey);
62
+ if (!list || list.length === 0)
63
+ return [];
64
+ return [...list]
65
+ .sort((a, b) => b.score - a.score)
66
+ .map((e) => ({ strategyId: e.strategyId, score: e.score }));
67
+ }
68
+ /**
69
+ * Get the best strategy for a blocker×app pair, or null if no data.
70
+ */
71
+ recommend(blockerType, bundleId, minSamples = 3) {
72
+ const compoundKey = `${blockerType}::${bundleId}`;
73
+ const list = this.entries.get(compoundKey);
74
+ if (!list || list.length === 0)
75
+ return null;
76
+ const qualified = list.filter((e) => e.successCount + e.failCount >= minSamples);
77
+ if (qualified.length === 0)
78
+ return null;
79
+ qualified.sort((a, b) => b.score - a.score);
80
+ return qualified[0].strategyId;
81
+ }
82
+ clear() {
83
+ this.entries.clear();
84
+ }
85
+ getAllEntries() {
86
+ const result = [];
87
+ for (const list of this.entries.values()) {
88
+ result.push(...list);
89
+ }
90
+ return result;
91
+ }
92
+ loadEntries(entries) {
93
+ for (const entry of entries) {
94
+ let list = this.entries.get(entry.key);
95
+ if (!list) {
96
+ list = [];
97
+ this.entries.set(entry.key, list);
98
+ }
99
+ const existing = list.find((e) => e.strategyId === entry.strategyId);
100
+ if (existing) {
101
+ existing.successCount = entry.successCount;
102
+ existing.failCount = entry.failCount;
103
+ existing.score = entry.score;
104
+ existing.avgDurationMs = entry.avgDurationMs;
105
+ existing.lastUsed = entry.lastUsed;
106
+ }
107
+ else {
108
+ list.push({ ...entry });
109
+ }
110
+ }
111
+ }
112
+ bayesianScore(successes, failures) {
113
+ return ((successes + this.priorStrength) /
114
+ (successes + failures + 2 * this.priorStrength));
115
+ }
116
+ }
@@ -0,0 +1,115 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * SensorPolicy — learns which perception source works best per app.
5
+ *
6
+ * Some apps are AX-friendly (VS Code, Finder), some need CDP (web apps),
7
+ * some need OCR (canvas-heavy apps like Canva, Premiere Pro).
8
+ * This policy tracks success rates and latency per source per app,
9
+ * so the perception coordinator can prioritize the best source.
10
+ */
11
+ export class SensorPolicy {
12
+ entries = new Map();
13
+ priorStrength;
14
+ constructor(priorStrength = 2) {
15
+ this.priorStrength = priorStrength;
16
+ }
17
+ /**
18
+ * Record a sensor outcome.
19
+ */
20
+ record(outcome) {
21
+ const key = `${outcome.bundleId}::${outcome.sourceType}`;
22
+ let entry = this.entries.get(key);
23
+ if (!entry) {
24
+ entry = {
25
+ key,
26
+ bundleId: outcome.bundleId,
27
+ sourceType: outcome.sourceType,
28
+ successCount: 0,
29
+ failCount: 0,
30
+ score: 0.5,
31
+ avgLatencyMs: 0,
32
+ lastUsed: new Date().toISOString(),
33
+ };
34
+ this.entries.set(key, entry);
35
+ }
36
+ if (outcome.success) {
37
+ entry.successCount++;
38
+ }
39
+ else {
40
+ entry.failCount++;
41
+ }
42
+ // Running average for latency — guard against NaN
43
+ const latency = Number.isFinite(outcome.latencyMs) ? outcome.latencyMs : 0;
44
+ const total = entry.successCount + entry.failCount;
45
+ entry.avgLatencyMs =
46
+ entry.avgLatencyMs * ((total - 1) / total) +
47
+ latency / total;
48
+ entry.score = this.bayesianScore(entry.successCount, entry.failCount);
49
+ entry.lastUsed = new Date().toISOString();
50
+ }
51
+ /** Known browser bundle IDs where CDP should be boosted */
52
+ static BROWSER_BUNDLES = new Set([
53
+ "com.apple.Safari", "com.google.Chrome", "com.brave.Browser",
54
+ "org.mozilla.firefox", "com.microsoft.edgemac",
55
+ "org.chromium.Chromium", "com.vivaldi.Vivaldi",
56
+ "com.operasoftware.Opera",
57
+ ]);
58
+ /**
59
+ * Rank perception sources for a given app, best first.
60
+ * Score combines reliability (Bayesian) and speed (lower latency = better).
61
+ * Browser apps get a CDP bootstrap boost when no CDP data exists yet.
62
+ */
63
+ rank(bundleId) {
64
+ const results = [];
65
+ for (const entry of this.entries.values()) {
66
+ if (entry.bundleId === bundleId) {
67
+ results.push({
68
+ sourceType: entry.sourceType,
69
+ score: entry.score,
70
+ avgLatencyMs: entry.avgLatencyMs,
71
+ });
72
+ }
73
+ }
74
+ // Bootstrap CDP for browser apps: if no CDP data exists yet,
75
+ // inject a prior so CDP isn't ranked at 0 for browsers
76
+ const isBrowser = SensorPolicy.BROWSER_BUNDLES.has(bundleId);
77
+ if (isBrowser && !results.some((r) => r.sourceType === "cdp")) {
78
+ results.push({ sourceType: "cdp", score: 0.7, avgLatencyMs: 100 });
79
+ }
80
+ // Sort by score descending, then by latency ascending for ties
81
+ results.sort((a, b) => {
82
+ const scoreDiff = b.score - a.score;
83
+ if (Math.abs(scoreDiff) > 0.05)
84
+ return scoreDiff;
85
+ return a.avgLatencyMs - b.avgLatencyMs;
86
+ });
87
+ return results;
88
+ }
89
+ /**
90
+ * Get the best source for a given app, or null if no data.
91
+ */
92
+ recommend(bundleId, minSamples = 5) {
93
+ const ranked = this.rank(bundleId);
94
+ const qualified = ranked.filter((r) => {
95
+ const entry = this.entries.get(`${bundleId}::${r.sourceType}`);
96
+ return entry && entry.successCount + entry.failCount >= minSamples;
97
+ });
98
+ return qualified.length > 0 ? qualified[0].sourceType : null;
99
+ }
100
+ clear() {
101
+ this.entries.clear();
102
+ }
103
+ getAllEntries() {
104
+ return [...this.entries.values()];
105
+ }
106
+ loadEntries(entries) {
107
+ for (const entry of entries) {
108
+ this.entries.set(entry.key, { ...entry });
109
+ }
110
+ }
111
+ bayesianScore(successes, failures) {
112
+ return ((successes + this.priorStrength) /
113
+ (successes + failures + 2 * this.priorStrength));
114
+ }
115
+ }
@@ -0,0 +1,204 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /** Default budgets from src/config.ts — used when insufficient data. */
4
+ const DEFAULT_LOCATE_MS = 800;
5
+ const DEFAULT_ACT_MS = 200;
6
+ const DEFAULT_VERIFY_MS = 2000;
7
+ /** Tools categorized by their role in the locate→act→verify pipeline. */
8
+ const LOCATE_TOOLS = new Set([
9
+ "ui_find", "locate_with_fallback", "browser_dom",
10
+ ]);
11
+ const ACT_TOOLS = new Set([
12
+ "click", "click_text", "click_with_fallback", "type_text",
13
+ "type_with_fallback", "key", "drag", "scroll", "scroll_with_fallback",
14
+ "browser_click", "browser_type", "browser_human_click",
15
+ "select_with_fallback", "ui_press", "ui_set_value", "menu_click",
16
+ ]);
17
+ const VERIFY_TOOLS = new Set([
18
+ "screenshot", "screenshot_file", "ocr", "ui_tree",
19
+ "browser_wait", "wait_for_state", "read_with_fallback",
20
+ "browser_page_info",
21
+ ]);
22
+ /**
23
+ * TimingModel — learns per-tool×app timing distributions and produces
24
+ * adaptive budgets that replace fixed defaults.
25
+ *
26
+ * Keeps a sliding window of samples per key. Computes p50/p95 lazily
27
+ * when a budget is requested.
28
+ */
29
+ export class TimingModel {
30
+ /** Map<compoundKey, TimingSample[]> — sliding window */
31
+ samples = new Map();
32
+ /** Cached distributions — invalidated on new sample */
33
+ distributions = new Map();
34
+ maxSamples;
35
+ constructor(maxSamples = 100) {
36
+ this.maxSamples = maxSamples;
37
+ }
38
+ /**
39
+ * Record a timing event.
40
+ */
41
+ record(event) {
42
+ const key = `${event.tool}::${event.bundleId}`;
43
+ let list = this.samples.get(key);
44
+ if (!list) {
45
+ list = [];
46
+ this.samples.set(key, list);
47
+ }
48
+ // Cap individual samples at 10s to prevent outliers (timeouts, stalls)
49
+ // from poisoning the adaptive budgets
50
+ const MAX_SAMPLE_MS = 10_000;
51
+ const rawDur = Number.isFinite(event.durationMs) && event.durationMs >= 0
52
+ ? event.durationMs
53
+ : 0;
54
+ const dur = Math.min(rawDur, MAX_SAMPLE_MS);
55
+ list.push({
56
+ tool: event.tool,
57
+ bundleId: event.bundleId,
58
+ durationMs: dur,
59
+ success: event.success,
60
+ timestamp: new Date().toISOString(),
61
+ });
62
+ // Sliding window: keep only recent samples
63
+ if (list.length > this.maxSamples) {
64
+ list.splice(0, list.length - this.maxSamples);
65
+ }
66
+ // Invalidate cached distribution
67
+ this.distributions.delete(key);
68
+ }
69
+ /**
70
+ * Get the timing distribution for a specific tool×app pair.
71
+ */
72
+ getDistribution(tool, bundleId) {
73
+ const key = `${tool}::${bundleId}`;
74
+ const cached = this.distributions.get(key);
75
+ if (cached)
76
+ return cached;
77
+ const list = this.samples.get(key);
78
+ if (!list || list.length === 0)
79
+ return null;
80
+ // Only use successful samples for timing (failures may have arbitrary durations)
81
+ const successDurations = list
82
+ .filter((s) => s.success)
83
+ .map((s) => s.durationMs);
84
+ if (successDurations.length === 0)
85
+ return null;
86
+ successDurations.sort((a, b) => a - b);
87
+ const dist = {
88
+ key,
89
+ sampleCount: successDurations.length,
90
+ p50: percentile(successDurations, 0.5),
91
+ p95: percentile(successDurations, 0.95),
92
+ mean: successDurations.reduce((a, b) => a + b, 0) / successDurations.length,
93
+ min: successDurations[0],
94
+ max: successDurations[successDurations.length - 1],
95
+ lastUpdated: new Date().toISOString(),
96
+ };
97
+ this.distributions.set(key, dist);
98
+ return dist;
99
+ }
100
+ /**
101
+ * Compute adaptive budgets for a given app by aggregating
102
+ * timing data across all tools of each category (locate/act/verify).
103
+ *
104
+ * Returns defaults for categories with insufficient data.
105
+ */
106
+ getAdaptiveBudget(bundleId, minSamples = 5) {
107
+ return {
108
+ locateMs: this.budgetForCategory(LOCATE_TOOLS, bundleId, DEFAULT_LOCATE_MS, minSamples),
109
+ actMs: this.budgetForCategory(ACT_TOOLS, bundleId, DEFAULT_ACT_MS, minSamples),
110
+ verifyMs: this.budgetForCategory(VERIFY_TOOLS, bundleId, DEFAULT_VERIFY_MS, minSamples),
111
+ };
112
+ }
113
+ /**
114
+ * Clear all samples and cached distributions.
115
+ */
116
+ clear() {
117
+ this.samples.clear();
118
+ this.distributions.clear();
119
+ }
120
+ /**
121
+ * Get all timing distributions (for persistence/inspection).
122
+ */
123
+ getAllDistributions() {
124
+ // Ensure all distributions are computed
125
+ for (const key of this.samples.keys()) {
126
+ if (!this.distributions.has(key)) {
127
+ const [tool, bundleId] = key.split("::");
128
+ if (tool && bundleId) {
129
+ this.getDistribution(tool, bundleId);
130
+ }
131
+ }
132
+ }
133
+ return [...this.distributions.values()];
134
+ }
135
+ /**
136
+ * Get all raw samples (for persistence).
137
+ */
138
+ getAllSamples() {
139
+ const result = [];
140
+ for (const list of this.samples.values()) {
141
+ result.push(...list);
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Load samples from persisted data.
147
+ */
148
+ loadSamples(samples) {
149
+ const MAX_SAMPLE_MS = 10_000;
150
+ for (const sample of samples) {
151
+ const key = `${sample.tool}::${sample.bundleId}`;
152
+ let list = this.samples.get(key);
153
+ if (!list) {
154
+ list = [];
155
+ this.samples.set(key, list);
156
+ }
157
+ // Cap loaded samples to prevent old poisoned data from inflating budgets
158
+ list.push({ ...sample, durationMs: Math.min(sample.durationMs, MAX_SAMPLE_MS) });
159
+ if (list.length > this.maxSamples) {
160
+ list.splice(0, list.length - this.maxSamples);
161
+ }
162
+ }
163
+ // Clear all cached distributions
164
+ this.distributions.clear();
165
+ }
166
+ /**
167
+ * Compute budget for a category of tools by taking the max p95
168
+ * across all tools in that category for the given app.
169
+ */
170
+ budgetForCategory(toolSet, bundleId, defaultMs, minSamples) {
171
+ let maxP95 = 0;
172
+ let hasData = false;
173
+ for (const tool of toolSet) {
174
+ const dist = this.getDistribution(tool, bundleId);
175
+ if (dist && dist.sampleCount >= minSamples) {
176
+ maxP95 = Math.max(maxP95, dist.p95);
177
+ hasData = true;
178
+ }
179
+ }
180
+ if (!hasData)
181
+ return defaultMs;
182
+ // Use p95 with a 20% margin, but never below the minimum sensible value
183
+ // and never above 5x the default to prevent budget explosion from outliers
184
+ const minFloor = defaultMs * 0.25;
185
+ const maxCeiling = defaultMs * 5;
186
+ return Math.min(Math.max(Math.ceil(maxP95 * 1.2), minFloor), maxCeiling);
187
+ }
188
+ }
189
+ /**
190
+ * Compute the p-th percentile of a sorted array.
191
+ */
192
+ function percentile(sorted, p) {
193
+ if (sorted.length === 0)
194
+ return 0;
195
+ if (sorted.length === 1)
196
+ return sorted[0];
197
+ const idx = p * (sorted.length - 1);
198
+ const lower = Math.floor(idx);
199
+ const upper = Math.ceil(idx);
200
+ if (lower === upper)
201
+ return sorted[lower];
202
+ const frac = idx - lower;
203
+ return sorted[lower] * (1 - frac) + sorted[upper] * frac;
204
+ }