@avasapp/agent-bridge 0.1.0 → 0.3.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 +168 -18
- package/dist/adapters/ably.cjs +344 -0
- package/dist/adapters/ably.d.cts +37 -0
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/socket-io.cjs +317 -0
- package/dist/adapters/socket-io.d.cts +21 -0
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.d.cts +1 -1
- package/dist/{chunk-UEWFQWCY.js → chunk-S2FYN2R5.js} +122 -2
- package/dist/cli.js +34 -45
- package/dist/client/index.cjs +109 -4
- package/dist/client/index.d.ts +44 -1
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +186 -21
- package/dist/network/index.d.cts +45 -2
- package/dist/noop/ably.cjs +26 -0
- package/dist/noop/index.cjs +9 -0
- package/dist/noop/network.cjs +7 -1
- package/dist/noop/realtime.cjs +40 -0
- package/dist/noop/socket-io.cjs +26 -0
- package/dist/realtime/index.cjs +279 -0
- package/dist/realtime/index.d.cts +138 -0
- package/dist/runtime/index.cjs +209 -13
- package/dist/runtime/index.d.cts +23 -3
- package/dist/{types-C6DUUHnB.d.cts → types-6kKQsLC1.d.cts} +40 -1
- package/entries/ably.cjs +9 -0
- package/entries/realtime.cjs +9 -0
- package/entries/socket.io.cjs +9 -0
- package/package.json +21 -4
- package/skills/agent-bridge/SKILL.md +68 -5
package/dist/runtime/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var runtime_exports = {};
|
|
|
22
22
|
__export(runtime_exports, {
|
|
23
23
|
RUNTIME_MARKER: () => RUNTIME_MARKER,
|
|
24
24
|
cdpTransport: () => cdpTransport,
|
|
25
|
+
createGate: () => createGate,
|
|
25
26
|
settle: () => settle,
|
|
26
27
|
startAgentBridge: () => startAgentBridge,
|
|
27
28
|
useAgentBridge: () => useAgentBridge
|
|
@@ -73,14 +74,15 @@ function logTools(logs) {
|
|
|
73
74
|
// src/runtime/tools/restore.ts
|
|
74
75
|
var SUFFIX = ".restore";
|
|
75
76
|
var SELF = "bridge.restore";
|
|
77
|
+
var LAST = "scenario.restore";
|
|
76
78
|
function restoreTools(getTools) {
|
|
77
79
|
return {
|
|
78
80
|
[SELF]: {
|
|
79
|
-
description: "Undo what the agent changed: runs every *.restore tool in name order and returns each result or error.",
|
|
81
|
+
description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
|
|
80
82
|
run: async () => {
|
|
81
83
|
const tools = getTools();
|
|
82
84
|
const results = {};
|
|
83
|
-
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort();
|
|
85
|
+
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
|
|
84
86
|
for (const name of names) {
|
|
85
87
|
const definition = tools[name];
|
|
86
88
|
const run = typeof definition === "function" ? definition : definition.run;
|
|
@@ -98,6 +100,159 @@ function restoreTools(getTools) {
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
// src/runtime/options-schema.ts
|
|
104
|
+
var import_json_schema = require("@cfworker/json-schema");
|
|
105
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
106
|
+
var CASCADE = /* @__PURE__ */ new Set([
|
|
107
|
+
"properties",
|
|
108
|
+
"patternProperties",
|
|
109
|
+
"additionalProperties",
|
|
110
|
+
"unevaluatedProperties",
|
|
111
|
+
"items",
|
|
112
|
+
"prefixItems",
|
|
113
|
+
"unevaluatedItems",
|
|
114
|
+
"allOf",
|
|
115
|
+
"if",
|
|
116
|
+
"then",
|
|
117
|
+
"else",
|
|
118
|
+
"dependentSchemas",
|
|
119
|
+
"$ref",
|
|
120
|
+
"false"
|
|
121
|
+
]);
|
|
122
|
+
var path = (instanceLocation) => instanceLocation === "#" ? "options" : `options${instanceLocation.slice(1)}`;
|
|
123
|
+
function unknownKeys(schema, value, at, out) {
|
|
124
|
+
if (!isObject(schema)) return;
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
if (isObject(schema.items))
|
|
127
|
+
value.forEach((item, i) => unknownKeys(schema.items, item, `${at}/${i}`, out));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!isObject(value) || !isObject(schema.properties)) return;
|
|
131
|
+
const props = schema.properties;
|
|
132
|
+
const open = schema.additionalProperties !== void 0 && schema.additionalProperties !== false || schema.patternProperties !== void 0 || // Composed schemas split their properties across branches; leave them to the validator.
|
|
133
|
+
["allOf", "anyOf", "oneOf", "$ref"].some((k) => schema[k] !== void 0);
|
|
134
|
+
for (const [key, child] of Object.entries(value)) {
|
|
135
|
+
if (key in props) unknownKeys(props[key], child, `${at}/${key}`, out);
|
|
136
|
+
else if (!open)
|
|
137
|
+
out.push(`${path(at)}: unknown option "${key}". Known: ${Object.keys(props).join(", ") || "none"}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
var validators = /* @__PURE__ */ new WeakMap();
|
|
141
|
+
function checkOptions(schema, options) {
|
|
142
|
+
let validator = validators.get(schema);
|
|
143
|
+
if (!validator) {
|
|
144
|
+
validator = new import_json_schema.Validator(schema, "2020-12", false);
|
|
145
|
+
validators.set(schema, validator);
|
|
146
|
+
}
|
|
147
|
+
const value = options === void 0 ? {} : options;
|
|
148
|
+
const problems = [];
|
|
149
|
+
unknownKeys(schema, value, "#", problems);
|
|
150
|
+
const result = validator.validate(value);
|
|
151
|
+
if (!result.valid) {
|
|
152
|
+
const leaves = result.errors.filter(
|
|
153
|
+
(e) => !CASCADE.has(e.keyword) && !/\/(anyOf|oneOf)\/\d/.test(e.keywordLocation)
|
|
154
|
+
);
|
|
155
|
+
const shown = leaves.length || problems.length ? leaves : result.errors;
|
|
156
|
+
for (const e of shown) problems.push(`${path(e.instanceLocation)}: ${e.error}`);
|
|
157
|
+
}
|
|
158
|
+
return [...new Set(problems)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/runtime/tools/scenario.ts
|
|
162
|
+
var active = /* @__PURE__ */ new Map();
|
|
163
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
164
|
+
async function runUndos(entry) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
for (const undo2 of entry.undos.splice(0).reverse()) {
|
|
167
|
+
try {
|
|
168
|
+
await undo2();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
errors.push(message(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return errors;
|
|
174
|
+
}
|
|
175
|
+
async function undo(name) {
|
|
176
|
+
const entry = active.get(name);
|
|
177
|
+
if (!entry) return [];
|
|
178
|
+
active.delete(name);
|
|
179
|
+
return (await runUndos(entry)).map((e) => `${name}: ${e}`);
|
|
180
|
+
}
|
|
181
|
+
function scenarioTools(getScenarios, getTools) {
|
|
182
|
+
const find = (name) => {
|
|
183
|
+
const scenarios = getScenarios();
|
|
184
|
+
const scenario = scenarios[name];
|
|
185
|
+
if (!scenario)
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Unknown scenario "${name}". Known: ${Object.keys(scenarios).join(", ") || "none"}`
|
|
188
|
+
);
|
|
189
|
+
return scenario;
|
|
190
|
+
};
|
|
191
|
+
const call = async (tool, ...args) => {
|
|
192
|
+
const definition = getTools()[tool];
|
|
193
|
+
if (!definition) throw new Error(`Unknown tool "${tool}"`);
|
|
194
|
+
const run = typeof definition === "function" ? definition : definition.run;
|
|
195
|
+
return run(...args);
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
"scenario.list": {
|
|
199
|
+
description: "Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.",
|
|
200
|
+
run: () => Object.entries(getScenarios()).map(([name, s]) => ({
|
|
201
|
+
name,
|
|
202
|
+
description: s.description,
|
|
203
|
+
options: s.options,
|
|
204
|
+
active: active.has(name)
|
|
205
|
+
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
206
|
+
},
|
|
207
|
+
"scenario.apply": {
|
|
208
|
+
description: "Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.",
|
|
209
|
+
run: async (name, options) => {
|
|
210
|
+
const scenario = find(name);
|
|
211
|
+
if (scenario.options) {
|
|
212
|
+
const problems = checkOptions(scenario.options, options);
|
|
213
|
+
if (problems.length)
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Scenario "${name}" got bad options, nothing was applied:
|
|
216
|
+
- ${problems.join("\n- ")}
|
|
217
|
+
scenario.list shows its options schema.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const previous = await undo(name);
|
|
221
|
+
if (previous.length)
|
|
222
|
+
throw new Error(`Undoing the active "${name}" failed: ${previous.join("; ")}`);
|
|
223
|
+
const entry = { options, undos: [] };
|
|
224
|
+
active.set(name, entry);
|
|
225
|
+
const context = {
|
|
226
|
+
options,
|
|
227
|
+
call,
|
|
228
|
+
onUndo: (fn) => {
|
|
229
|
+
if (typeof fn !== "function")
|
|
230
|
+
throw new Error("onUndo takes a function");
|
|
231
|
+
entry.undos.push(fn);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
return await scenario.apply(context);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const errors = await undo(name);
|
|
238
|
+
const extra = errors.length ? ` (undo: ${errors.join("; ")})` : "";
|
|
239
|
+
throw new Error(`Scenario "${name}" failed: ${message(error)}${extra}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"scenario.restore": {
|
|
244
|
+
description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
|
|
245
|
+
run: async () => {
|
|
246
|
+
const names = [...active.keys()].reverse();
|
|
247
|
+
const errors = [];
|
|
248
|
+
for (const name of names) errors.push(...await undo(name));
|
|
249
|
+
if (errors.length) throw new Error(errors.join("; "));
|
|
250
|
+
return names;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
101
256
|
// src/runtime/tools/screen.ts
|
|
102
257
|
var import_react_native = require("react-native");
|
|
103
258
|
|
|
@@ -639,12 +794,14 @@ function screenTools() {
|
|
|
639
794
|
}
|
|
640
795
|
|
|
641
796
|
// src/runtime/builtin-tools.ts
|
|
642
|
-
function builtinTools(listTools, getTools, logs) {
|
|
797
|
+
function builtinTools(listTools, getTools, logs, getScenarios) {
|
|
643
798
|
return {
|
|
644
799
|
...bridgeTools(listTools),
|
|
645
800
|
...logTools(logs),
|
|
646
801
|
...restoreTools(getTools),
|
|
647
|
-
...screenTools()
|
|
802
|
+
...screenTools(),
|
|
803
|
+
// Only when the app defines scenarios, so `tools` doesn't list empty ones.
|
|
804
|
+
...getScenarios && scenarioTools(getScenarios, getTools)
|
|
648
805
|
};
|
|
649
806
|
}
|
|
650
807
|
|
|
@@ -673,6 +830,38 @@ function cdpTransport() {
|
|
|
673
830
|
};
|
|
674
831
|
}
|
|
675
832
|
|
|
833
|
+
// src/runtime/gate.ts
|
|
834
|
+
function createGate(name) {
|
|
835
|
+
let holds = 0;
|
|
836
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
837
|
+
const notify = () => {
|
|
838
|
+
for (const fn of [...listeners]) fn(holds > 0);
|
|
839
|
+
};
|
|
840
|
+
return {
|
|
841
|
+
name,
|
|
842
|
+
get closed() {
|
|
843
|
+
return holds > 0;
|
|
844
|
+
},
|
|
845
|
+
close: () => {
|
|
846
|
+
let released = false;
|
|
847
|
+
holds += 1;
|
|
848
|
+
if (holds === 1) notify();
|
|
849
|
+
return () => {
|
|
850
|
+
if (released) return;
|
|
851
|
+
released = true;
|
|
852
|
+
holds -= 1;
|
|
853
|
+
if (holds === 0) notify();
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
subscribe: (fn) => {
|
|
857
|
+
listeners.add(fn);
|
|
858
|
+
return () => {
|
|
859
|
+
listeners.delete(fn);
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
676
865
|
// src/runtime/logs.ts
|
|
677
866
|
var CAPACITY = 200;
|
|
678
867
|
var STACK_LINES = 10;
|
|
@@ -735,9 +924,9 @@ function formatArgs(args) {
|
|
|
735
924
|
}
|
|
736
925
|
parts.push(...rest.map(describe2));
|
|
737
926
|
const error = args.find((a) => a instanceof Error);
|
|
738
|
-
const
|
|
927
|
+
const message2 = parts.join(" ");
|
|
739
928
|
return {
|
|
740
|
-
message:
|
|
929
|
+
message: message2.length > MESSAGE_CHARS ? `${message2.slice(0, MESSAGE_CHARS)}...` : message2,
|
|
741
930
|
...error?.stack ? { stack: trimStack(error.stack) } : {}
|
|
742
931
|
};
|
|
743
932
|
}
|
|
@@ -756,9 +945,9 @@ function describe2(value) {
|
|
|
756
945
|
var trimStack = (stack) => stack.split("\n").slice(0, STACK_LINES).join("\n");
|
|
757
946
|
function installLogHooks(capture, env) {
|
|
758
947
|
let quiet = 0;
|
|
759
|
-
let
|
|
948
|
+
let active2 = true;
|
|
760
949
|
const record = (level, args) => {
|
|
761
|
-
if (!
|
|
950
|
+
if (!active2 || quiet) return;
|
|
762
951
|
quiet++;
|
|
763
952
|
try {
|
|
764
953
|
capture.record(level, args);
|
|
@@ -801,7 +990,7 @@ function installLogHooks(capture, env) {
|
|
|
801
990
|
env.addEventListener?.("unhandledrejection", onRejection);
|
|
802
991
|
}
|
|
803
992
|
return () => {
|
|
804
|
-
|
|
993
|
+
active2 = false;
|
|
805
994
|
if (console.error === wrappers.error) console.error = originals.error;
|
|
806
995
|
if (console.warn === wrappers.warn) console.warn = originals.warn;
|
|
807
996
|
if (errorUtils && originalHandler && errorUtils.getGlobalHandler() === handler2)
|
|
@@ -877,8 +1066,8 @@ function createRegistry(getTools, logs) {
|
|
|
877
1066
|
const value = await unwrap(definition).run(...call.args ?? []);
|
|
878
1067
|
return { id: call.id, from, ok: true, value: toJson(value), ms: ms() };
|
|
879
1068
|
} catch (error) {
|
|
880
|
-
const
|
|
881
|
-
return { id: call.id, from, ok: false, error:
|
|
1069
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1070
|
+
return { id: call.id, from, ok: false, error: message2, ms: ms() };
|
|
882
1071
|
}
|
|
883
1072
|
}
|
|
884
1073
|
return { list, dispatch };
|
|
@@ -889,9 +1078,11 @@ var randomId = () => Math.random().toString(36).slice(2, 10);
|
|
|
889
1078
|
function startAgentBridge(options = {}) {
|
|
890
1079
|
const deviceId = randomId();
|
|
891
1080
|
const userTools = () => typeof options.tools === "function" ? options.tools() : options.tools ?? {};
|
|
1081
|
+
const { scenarios } = options;
|
|
1082
|
+
const getScenarios = scenarios ? () => typeof scenarios === "function" ? scenarios() : scenarios : void 0;
|
|
892
1083
|
const logs = startLogCapture();
|
|
893
1084
|
const allTools = () => ({
|
|
894
|
-
...builtinTools(() => registry.list(), allTools, logs.capture),
|
|
1085
|
+
...builtinTools(() => registry.list(), allTools, logs.capture, getScenarios),
|
|
895
1086
|
...userTools()
|
|
896
1087
|
});
|
|
897
1088
|
const registry = createRegistry(allTools, logs.capture);
|
|
@@ -927,7 +1118,12 @@ function useAgentBridge(options = {}) {
|
|
|
927
1118
|
tools: () => {
|
|
928
1119
|
const { tools } = latest.current;
|
|
929
1120
|
return typeof tools === "function" ? tools() : tools ?? {};
|
|
930
|
-
}
|
|
1121
|
+
},
|
|
1122
|
+
// Decided at mount: scenario.* tools are there or not for the bridge's life.
|
|
1123
|
+
scenarios: latest.current.scenarios ? () => {
|
|
1124
|
+
const { scenarios } = latest.current;
|
|
1125
|
+
return typeof scenarios === "function" ? scenarios() : scenarios ?? {};
|
|
1126
|
+
} : void 0
|
|
931
1127
|
});
|
|
932
1128
|
}, []);
|
|
933
1129
|
}
|
package/dist/runtime/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Transport, A as AgentBridgeOptions } from '../types-
|
|
2
|
-
export { C as CallMessage, D as DeviceInfo, L as LogEntry, R as RUNTIME_MARKER, b as ResultMessage, c as
|
|
1
|
+
import { a as Transport, A as AgentBridgeOptions } from '../types-6kKQsLC1.cjs';
|
|
2
|
+
export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-6kKQsLC1.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Works in any React Native app served by Metro. The client evaluates one fixed
|
|
@@ -8,6 +8,26 @@ export { C as CallMessage, D as DeviceInfo, L as LogEntry, R as RUNTIME_MARKER,
|
|
|
8
8
|
*/
|
|
9
9
|
declare function cdpTransport(): Transport;
|
|
10
10
|
|
|
11
|
+
type Gate = {
|
|
12
|
+
name: string;
|
|
13
|
+
/** True while anything holds the gate closed. */
|
|
14
|
+
readonly closed: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Closes the gate until the returned function is called. Closes are
|
|
17
|
+
* counted, so the gate opens when the last one is released. Hand the
|
|
18
|
+
* result to a scenario's `onUndo`.
|
|
19
|
+
*/
|
|
20
|
+
close: () => () => void;
|
|
21
|
+
/** Calls `fn(closed)` when the gate closes or opens. Returns an unsubscribe. */
|
|
22
|
+
subscribe: (fn: (closed: boolean) => void) => () => void;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A switch app code checks before a side effect a scenario must hold off,
|
|
26
|
+
* e.g. connecting a realtime client with a fake token. Always open in a
|
|
27
|
+
* release build.
|
|
28
|
+
*/
|
|
29
|
+
declare function createGate(name: string): Gate;
|
|
30
|
+
|
|
11
31
|
type SettleResult = {
|
|
12
32
|
commits: number;
|
|
13
33
|
ms: number;
|
|
@@ -66,4 +86,4 @@ declare function startAgentBridge(options?: AgentBridgeOptions): () => void;
|
|
|
66
86
|
*/
|
|
67
87
|
declare function useAgentBridge(options?: AgentBridgeOptions): void;
|
|
68
88
|
|
|
69
|
-
export { AgentBridgeOptions, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, settle, startAgentBridge, useAgentBridge };
|
|
89
|
+
export { AgentBridgeOptions, type Gate, type ScreenElement, type SettleResult, type Target, Transport, cdpTransport, createGate, settle, startAgentBridge, useAgentBridge };
|
|
@@ -48,6 +48,14 @@ type DeviceInfo = {
|
|
|
48
48
|
tools: ToolInfo[];
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A JSON Schema (draft 2020-12) for a scenario's options. Typed loosely, so
|
|
53
|
+
* `as const` schemas and ones generated by other libraries fit.
|
|
54
|
+
*/
|
|
55
|
+
type OptionsSchema = {
|
|
56
|
+
readonly [keyword: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
type ToolFn = (...args: any[]) => unknown;
|
|
52
60
|
type ToolDefinition = ToolFn | {
|
|
53
61
|
description?: string;
|
|
@@ -64,13 +72,44 @@ type Transport = {
|
|
|
64
72
|
/** Starts listening; returns a function that stops. */
|
|
65
73
|
start: (context: TransportContext) => () => void;
|
|
66
74
|
};
|
|
75
|
+
type ScenarioContext<O = unknown> = {
|
|
76
|
+
/** What the agent or flow passed to `scenario.apply`, or undefined. */
|
|
77
|
+
options: O | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Calls any bridge tool by name, e.g. `call('store.set', 'auth', {...})`.
|
|
80
|
+
* What a tool changes is undone by its own restorer in `bridge.restore`.
|
|
81
|
+
*/
|
|
82
|
+
call: (tool: string, ...args: unknown[]) => Promise<unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Runs `fn` when the scenario is undone, after every other restorer. Use it
|
|
85
|
+
* for anything the tools' restorers don't cover: app mocks, gates, native
|
|
86
|
+
* state. Undo callbacks run newest first.
|
|
87
|
+
*/
|
|
88
|
+
onUndo: (fn: () => unknown) => void;
|
|
89
|
+
};
|
|
90
|
+
/** A named setup the app defines and the agent applies, e.g. a signed-in user. */
|
|
91
|
+
type Scenario<O = unknown> = {
|
|
92
|
+
description?: string;
|
|
93
|
+
/**
|
|
94
|
+
* JSON Schema (2020-12) for `options`. `scenario.apply` rejects options
|
|
95
|
+
* that don't fit before `apply` runs, and `scenario.list` shows it. Keys an
|
|
96
|
+
* object schema doesn't list are rejected unless it sets
|
|
97
|
+
* `additionalProperties`. No options are checked as `{}`.
|
|
98
|
+
*/
|
|
99
|
+
options?: OptionsSchema;
|
|
100
|
+
/** Sets the app up. Its return value goes back to the agent. */
|
|
101
|
+
apply: (context: ScenarioContext<O>) => unknown;
|
|
102
|
+
};
|
|
103
|
+
type Scenarios = Record<string, Scenario<any>>;
|
|
67
104
|
type AgentBridgeOptions = {
|
|
68
105
|
/** Your tools, or a function returning them (read on every call). */
|
|
69
106
|
tools?: Tools | (() => Tools);
|
|
107
|
+
/** Named setups for `scenario.*`, or a function returning them (read on every call). */
|
|
108
|
+
scenarios?: Scenarios | (() => Scenarios);
|
|
70
109
|
/** How this app shows up in `agent-bridge devices`. */
|
|
71
110
|
name?: string;
|
|
72
111
|
/** Defaults to CDP only. Add `expoTransport()` on Expo. */
|
|
73
112
|
transports?: Transport[];
|
|
74
113
|
};
|
|
75
114
|
|
|
76
|
-
export { type AgentBridgeOptions as A, type CallMessage as C, type DeviceInfo as D, type LogEntry as L, RUNTIME_MARKER as R, type Tools as T, type Transport as a, type ResultMessage as b, type
|
|
115
|
+
export { type AgentBridgeOptions as A, type CallMessage as C, type DeviceInfo as D, type LogEntry as L, type OptionsSchema as O, RUNTIME_MARKER as R, type Scenario as S, type Tools as T, type Transport as a, type ResultMessage as b, type ScenarioContext as c, type Scenarios as d, type ToolDefinition as e, type ToolFn as f, type ToolInfo as g };
|
package/entries/ably.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/ably.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/ably.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/realtime.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/realtime/index.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/socket-io.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/socket-io.cjs')
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avasapp/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/avas-app/agent-bridge/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
"provenance": true
|
|
16
|
+
"access": "public"
|
|
18
17
|
},
|
|
19
18
|
"engines": {
|
|
20
19
|
"node": ">=20"
|
|
@@ -51,6 +50,18 @@
|
|
|
51
50
|
"types": "./dist/network/index.d.cts",
|
|
52
51
|
"default": "./entries/network.cjs"
|
|
53
52
|
},
|
|
53
|
+
"./realtime": {
|
|
54
|
+
"types": "./dist/realtime/index.d.cts",
|
|
55
|
+
"default": "./entries/realtime.cjs"
|
|
56
|
+
},
|
|
57
|
+
"./ably": {
|
|
58
|
+
"types": "./dist/adapters/ably.d.cts",
|
|
59
|
+
"default": "./entries/ably.cjs"
|
|
60
|
+
},
|
|
61
|
+
"./socket.io": {
|
|
62
|
+
"types": "./dist/adapters/socket-io.d.cts",
|
|
63
|
+
"default": "./entries/socket.io.cjs"
|
|
64
|
+
},
|
|
54
65
|
"./client": {
|
|
55
66
|
"types": "./dist/client/index.d.ts",
|
|
56
67
|
"import": "./dist/client/index.js",
|
|
@@ -77,7 +88,10 @@
|
|
|
77
88
|
"cdp",
|
|
78
89
|
"tanstack-query",
|
|
79
90
|
"zustand",
|
|
80
|
-
"mmkv"
|
|
91
|
+
"mmkv",
|
|
92
|
+
"realtime",
|
|
93
|
+
"ably",
|
|
94
|
+
"socket.io"
|
|
81
95
|
],
|
|
82
96
|
"scripts": {
|
|
83
97
|
"build": "rm -rf dist && tsup",
|
|
@@ -104,6 +118,7 @@
|
|
|
104
118
|
}
|
|
105
119
|
},
|
|
106
120
|
"dependencies": {
|
|
121
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
107
122
|
"ws": "^8.21.3"
|
|
108
123
|
},
|
|
109
124
|
"devDependencies": {
|
|
@@ -113,6 +128,8 @@
|
|
|
113
128
|
"@types/bun": "^1.4.2",
|
|
114
129
|
"@types/react": "^19.3.0",
|
|
115
130
|
"@types/ws": "^8.18.1",
|
|
131
|
+
"ably": "^2.29.0",
|
|
132
|
+
"socket.io-client": "^4.8.4",
|
|
116
133
|
"tsup": "^8.5.1",
|
|
117
134
|
"typescript": "~6.0.3",
|
|
118
135
|
"zustand": "^5.0.15"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-bridge
|
|
3
|
-
description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network and undo it all, in milliseconds per call instead of tapping through the app. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
|
|
3
|
+
description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: start from an app-defined scenario such as a locally signed-in user, see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network, fake realtime messages and undo it all, in milliseconds per call instead of tapping through the app. Pairs with agent-device, which opens the app and covers native UI, real taps and screenshots. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Driving an app with agent-bridge
|
|
@@ -8,6 +8,22 @@ description: "Drive a running React Native or Expo dev build directly through @a
|
|
|
8
8
|
The app mounts `useAgentBridge({ tools })` in a dev build. You call those tools
|
|
9
9
|
by name. Nothing is tapped, so there is no setup to click through.
|
|
10
10
|
|
|
11
|
+
## Pair it with agent-device
|
|
12
|
+
|
|
13
|
+
agent-bridge works inside the app's JavaScript; agent-device works the device.
|
|
14
|
+
Use both, each for what it's good at:
|
|
15
|
+
|
|
16
|
+
| agent-bridge | agent-device |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Put the app in a state: flags, query data, store values, network mocks, realtime messages | Install and open the app, reload, relaunch |
|
|
19
|
+
| Navigate, fill forms, press buttons, check text on screen | System alerts, permission prompts, native sheets, the keyboard |
|
|
20
|
+
| Undo it all with `bridge.restore` | Screenshots, recordings, one real tap to prove the UI responds |
|
|
21
|
+
|
|
22
|
+
A typical run: `agent-device open` the app, set up and check the screen with
|
|
23
|
+
agent-bridge, then finish with one real tap or a screenshot from agent-device.
|
|
24
|
+
Don't tap through setup screens or edit backend data to reach a state that one
|
|
25
|
+
bridge call can set.
|
|
26
|
+
|
|
11
27
|
## Start a session
|
|
12
28
|
|
|
13
29
|
```sh
|
|
@@ -21,6 +37,30 @@ minutes without calls and runs `bridge.restore` when it does.
|
|
|
21
37
|
If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
|
|
22
38
|
this build. Say so; don't fall back to tapping silently.
|
|
23
39
|
|
|
40
|
+
## Start from a scenario
|
|
41
|
+
|
|
42
|
+
Apps can define named setups, such as a locally signed-in user. Use one
|
|
43
|
+
instead of signing in by hand or seeding auth state yourself:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx agent-bridge scenarios # what this app defines
|
|
47
|
+
npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`scenarios` shows each one's options as a JSON Schema. Bad options fail
|
|
51
|
+
before anything changes, and the error lists every problem
|
|
52
|
+
(`options: unknown option "usr". Known: user`): fix them and apply again.
|
|
53
|
+
`bridge.restore` (and `session stop`) undoes it after everything else. A local
|
|
54
|
+
scenario usually turns on strict network mode: any request no mock answers
|
|
55
|
+
fails with a 501, and the reply carries an error that names it
|
|
56
|
+
(`! error during …: agent-bridge strict network: no mock for GET https://…`).
|
|
57
|
+
Add a mock for it (`net.mock`), don't turn strict mode off.
|
|
58
|
+
`npx agent-bridge call net.strict` lists what it blocked. Strict mode sees JS
|
|
59
|
+
`fetch`/XHR only, not images, WebSockets or native SDKs.
|
|
60
|
+
|
|
61
|
+
If the app has no scenario for the state you need, say so. Don't sign in to a
|
|
62
|
+
real account instead.
|
|
63
|
+
|
|
24
64
|
## See and use the screen
|
|
25
65
|
|
|
26
66
|
```sh
|
|
@@ -32,7 +72,7 @@ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true}
|
|
|
32
72
|
|
|
33
73
|
`press` and `fill` return once the app has rendered the result, so the next
|
|
34
74
|
check sees it. `fill` skips the keyboard: autocorrect and native-only input
|
|
35
|
-
behaviour need one real typing step from
|
|
75
|
+
behaviour need one real typing step from agent-device.
|
|
36
76
|
|
|
37
77
|
## Get the app into a state
|
|
38
78
|
|
|
@@ -48,6 +88,25 @@ npx agent-bridge call net.log
|
|
|
48
88
|
Arguments are a JSON array (or a single JSON value). Read the current value
|
|
49
89
|
first (`query.get`, `store.get`) and change only what you need.
|
|
50
90
|
|
|
91
|
+
## Realtime messages
|
|
92
|
+
|
|
93
|
+
If the app has `realtime.*` tools, you can see its realtime messages and fake
|
|
94
|
+
them instead of waiting for a backend to send one:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npx agent-bridge call realtime.channels # what the app listens on
|
|
98
|
+
npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages, real and faked
|
|
99
|
+
npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
|
|
100
|
+
npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
|
|
101
|
+
npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Copy the shape of a real message from `realtime.log` before you emit one. With
|
|
105
|
+
Ably, pass `{ name, data }`; with socket.io, pass the arguments after the
|
|
106
|
+
channel (the event name). Muting still lets your emitted messages through.
|
|
107
|
+
`emit` fails and lists the live channels when nothing listens on yours: open
|
|
108
|
+
the screen that subscribes first.
|
|
109
|
+
|
|
51
110
|
## Watch for errors
|
|
52
111
|
|
|
53
112
|
Every reply carries errors the app logged or threw since the previous reply,
|
|
@@ -60,6 +119,8 @@ last 200 errors and warnings.
|
|
|
60
119
|
Write the steps into a flow file and run it:
|
|
61
120
|
|
|
62
121
|
```js
|
|
122
|
+
export const scenario = 'signedIn' // applied first, undone after, even on failure
|
|
123
|
+
|
|
63
124
|
export default async ({ step }) => {
|
|
64
125
|
await step('open form', 'screen.press', 'add-plant')
|
|
65
126
|
await step('save empty', 'screen.press', 'save-plant')
|
|
@@ -70,6 +131,7 @@ export default async ({ step }) => {
|
|
|
70
131
|
|
|
71
132
|
```sh
|
|
72
133
|
npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
|
|
134
|
+
npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
|
|
73
135
|
```
|
|
74
136
|
|
|
75
137
|
## Clean up
|
|
@@ -78,12 +140,13 @@ npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an
|
|
|
78
140
|
npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
|
|
79
141
|
```
|
|
80
142
|
|
|
81
|
-
`bridge.restore` unpins queries, puts back store and MMKV values
|
|
82
|
-
network mocks,
|
|
143
|
+
`bridge.restore` unpins queries, puts back store and MMKV values, removes
|
|
144
|
+
network mocks, unmutes realtime channels and ends a faked connection state,
|
|
145
|
+
then undoes active scenarios, so the next run starts from the real state.
|
|
83
146
|
|
|
84
147
|
## Traps
|
|
85
148
|
|
|
86
149
|
- Run next to the simulator. From another machine every call pays the network.
|
|
87
150
|
- It can't see native UI (system alerts, permission prompts, native sheets) or
|
|
88
|
-
overlap. Finish a flow with one real UI check or screenshot from
|
|
151
|
+
overlap. Finish a flow with one real UI check or screenshot from agent-device.
|
|
89
152
|
- With several apps on one Metro, pass `--device`.
|