@mobileaidev/ai-app-bridge 0.3.7 → 0.4.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 +52 -39
- package/bin/ai-app-bridge.js +56 -17
- package/bin/command-discovery.js +19 -4
- package/bin/command-registry.js +21 -6
- package/bin/command-request.js +68 -4
- package/bin/execution-host.js +32 -5
- package/bin/execution-runtime.js +17 -10
- package/bin/executors/command-schema.js +1 -1
- package/bin/executors/preparation.js +262 -0
- package/bin/extraction/json-value.js +26 -0
- package/bin/extraction/prepare.js +57 -0
- package/bin/extraction/regex.js +30 -0
- package/bin/extraction/runner.js +77 -0
- package/bin/intent/install-intent.js +3 -1
- package/bin/ios-provider.js +2 -2
- package/bin/ios-wda-project.js +48 -1
- package/bin/mcp-server.js +15 -20
- package/bin/public-reply.js +184 -0
- package/bin/response-store.js +60 -0
- package/bin/runtime-client.js +32 -15
- package/bin/runtime-directory.js +37 -8
- package/bin/script/node-runtime-adapter.js +139 -123
- package/bin/script/python-runtime-adapter.js +1 -1
- package/bin/script/script-diagnostics.js +21 -0
- package/bin/script/script-durable-restore.js +1 -0
- package/bin/script/script-host-port.js +3 -2
- package/bin/script/script-sdk.js +39 -4
- package/bin/script/script-sdk.py +79 -7
- package/bin/script/script-session-channel.js +27 -10
- package/bin/script/script-supervisor.js +8 -0
- package/bin/shared-kernel/argument-schema.js +44 -12
- package/bin/shared-kernel/evidence-archive.js +2 -2
- package/bin/shared-kernel/evidence-schema.js +16 -1
- package/bin/shared-kernel/evidence-store.js +3 -3
- package/bin/shared-kernel/execution-contracts.js +13 -5
- package/bin/shared-kernel/execution-target.js +4 -0
- package/bin/shared-kernel/request-context.js +2 -2
- package/bin/target-execution.js +2 -0
- package/docs/COMMAND_CONTRACT.md +91 -19
- package/docs/EVIDENCE_ARCHIVE.md +14 -1
- package/docs/INSTALLATION.md +73 -0
- package/docs/INTENT_FOREGROUND.md +4 -1
- package/docs/OPTIONAL_EXECUTORS.md +41 -13
- package/docs/RELEASE.md +60 -113
- package/docs/RESPONSE_EXTRACTION.md +122 -0
- package/docs/SCRIPT_AUTHORING.md +125 -6
- package/node_modules/@mobileaidev/segmented-fact-store-native/PREBUILDS.md +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding-path.js +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +1 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +1 -3
- package/node_modules/@mobileaidev/segmented-fact-store-native/install.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +11 -5
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-arm64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-x64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-arm64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-x64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/manifest.json +27 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/build-release-prebuilds.js +33 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/stage-prebuild.js +17 -0
- package/package.json +12 -5
- package/runtime/executors/android/prepare.init.gradle +92 -0
- package/runtime/executors/playwright/package-lock.json +2 -2
- package/runtime/executors/playwright/package.json +1 -1
- package/skills/ai-app-bridge-use/SKILL.md +19 -4
|
@@ -38,174 +38,190 @@ function createChildRuntimeAdapter({
|
|
|
38
38
|
onSpawnFailure,
|
|
39
39
|
} = {}) {
|
|
40
40
|
let channel = null;
|
|
41
|
+
let child = null;
|
|
41
42
|
let exited = Promise.resolve();
|
|
42
43
|
let readControl = () => ({ status: 'running', pauseReason: null });
|
|
43
44
|
|
|
44
45
|
async function terminate() {
|
|
45
46
|
if (channel) channel.stop();
|
|
46
47
|
await exited;
|
|
48
|
+
child?.stdin?.destroy(); child?.stdout?.destroy(); child?.stderr?.destroy();
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function start(args) {
|
|
50
|
-
|
|
51
|
+
async function start(args) {
|
|
52
|
+
const result = await inRequestDirectory(args.spec.cwd ?? requestDirectory(), () => startInDirectory(args));
|
|
53
|
+
if (!result.ok && channel) {
|
|
54
|
+
const diagnostics = { ...channel.diagnostics(), ...result.diagnostics };
|
|
55
|
+
if (Object.keys(diagnostics).length) result.diagnostics = diagnostics;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
async function startInDirectory({ spec, host, agent, emit, control }) {
|
|
54
61
|
readControl = typeof control === 'function' ? control : () => control || { status: 'running', pauseReason: null };
|
|
55
|
-
|
|
56
|
-
const artifact = path.join(directory, `main${extension}`);
|
|
57
|
-
fs.writeFileSync(artifact, spec.source);
|
|
58
|
-
const sdk = path.join(__dirname, sdkFile);
|
|
59
|
-
const child = spawn(executable, argsPrefix.concat([sdk, artifact]), {
|
|
60
|
-
shell: false,
|
|
61
|
-
cwd: requestDirectory(),
|
|
62
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
-
});
|
|
64
|
-
channel = createScriptSessionChannel(child, {
|
|
65
|
-
maxFrameBytes: Math.max(spec.policy.maxOutputBytes, spec.policy.maxProgressBytes, 1024 * 1024) + 64 * 1024,
|
|
66
|
-
});
|
|
67
|
-
exited = childExitPromise(child, onSpawnFailure);
|
|
68
|
-
const finished = exited;
|
|
62
|
+
let directory;
|
|
69
63
|
let timer = null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
const rpc = pending => Promise.race([pending, timedOut, finished.then(() => { throw new Error('child_crashed'); })]);
|
|
64
|
+
channel = null;
|
|
65
|
+
exited = Promise.resolve();
|
|
74
66
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
while (true) {
|
|
96
|
-
const message = await Promise.race([
|
|
67
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'aab-script-'));
|
|
68
|
+
const artifact = path.join(directory, `main${extension}`);
|
|
69
|
+
fs.writeFileSync(artifact, spec.source);
|
|
70
|
+
const sdk = path.join(__dirname, sdkFile);
|
|
71
|
+
child = spawn(executable, argsPrefix.concat([sdk, artifact]), {
|
|
72
|
+
shell: false,
|
|
73
|
+
cwd: requestDirectory(),
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
channel = createScriptSessionChannel(child, {
|
|
77
|
+
maxFrameBytes: Math.max(spec.policy.maxOutputBytes, spec.policy.maxProgressBytes, 1024 * 1024) + 64 * 1024,
|
|
78
|
+
});
|
|
79
|
+
exited = childExitPromise(child, onSpawnFailure);
|
|
80
|
+
const finished = exited;
|
|
81
|
+
const timedOut = new Promise((resolve) => {
|
|
82
|
+
timer = setTimeout(() => resolve({ type: 'timeout' }), spec.policy.timeoutMs);
|
|
83
|
+
});
|
|
84
|
+
const rpc = pending => Promise.race([pending, timedOut, finished.then(() => { throw new Error('child_crashed'); })]);
|
|
85
|
+
const ready = await Promise.race([
|
|
97
86
|
channel.nextMessage(),
|
|
98
87
|
finished,
|
|
99
88
|
timedOut,
|
|
100
89
|
]);
|
|
101
|
-
if (
|
|
90
|
+
if (ready && ready.type === 'timeout') {
|
|
102
91
|
await terminate();
|
|
103
|
-
emit('script_failed', { error: 'timeout' });
|
|
104
92
|
return { ok: false, error: 'timeout' };
|
|
105
93
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await terminate();
|
|
110
|
-
emit('script_failed', { error: 'timeout' });
|
|
111
|
-
return { ok: false, error: 'timeout' };
|
|
112
|
-
}
|
|
113
|
-
channel.send({ type: 'result', id: message.id, value, control: readControl() });
|
|
114
|
-
continue;
|
|
94
|
+
if (!ready || ready.type !== 'ready') {
|
|
95
|
+
await terminate();
|
|
96
|
+
return { ok: false, error: ready && ready.type === 'fail' && ready.error === 'handshake_failed' ? 'handshake_failed' : 'child_crashed' };
|
|
115
97
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
emit('child_started', { kind });
|
|
99
|
+
channel.send({
|
|
100
|
+
type: 'start',
|
|
101
|
+
inputs: spec.inputs,
|
|
102
|
+
entrypoint: spec.entrypoint,
|
|
103
|
+
sourceName: spec.sourcePath || artifact,
|
|
104
|
+
control: control(),
|
|
105
|
+
});
|
|
106
|
+
while (true) {
|
|
107
|
+
const message = await Promise.race([
|
|
108
|
+
channel.nextMessage(),
|
|
109
|
+
finished,
|
|
110
|
+
timedOut,
|
|
111
|
+
]);
|
|
112
|
+
if (message.type === 'timeout') {
|
|
119
113
|
await terminate();
|
|
120
114
|
emit('script_failed', { error: 'timeout' });
|
|
121
115
|
return { ok: false, error: 'timeout' };
|
|
122
116
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await terminate();
|
|
133
|
-
emit('script_failed', { error: 'progress_too_large' });
|
|
134
|
-
return { ok: false, error: 'progress_too_large' };
|
|
117
|
+
if (message.type === 'call') {
|
|
118
|
+
const value = await rpc(host.call(message.command, message.args, message.options));
|
|
119
|
+
if (value && value.type === 'timeout') {
|
|
120
|
+
await terminate();
|
|
121
|
+
emit('script_failed', { error: 'timeout' });
|
|
122
|
+
return { ok: false, error: 'timeout' };
|
|
123
|
+
}
|
|
124
|
+
channel.send({ type: 'result', id: message.id, value, control: readControl() });
|
|
125
|
+
continue;
|
|
135
126
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
127
|
+
if (message.type === 'assert') {
|
|
128
|
+
const value = await rpc(host.assert(message.assertion));
|
|
129
|
+
if (value && value.type === 'timeout') {
|
|
130
|
+
await terminate();
|
|
131
|
+
emit('script_failed', { error: 'timeout' });
|
|
132
|
+
return { ok: false, error: 'timeout' };
|
|
133
|
+
}
|
|
134
|
+
channel.send({ type: 'verdict', id: message.id, value, control: readControl() });
|
|
135
|
+
continue;
|
|
144
136
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
emit('script_failed', { error: 'timeout' });
|
|
149
|
-
return { ok: false, error: 'timeout' };
|
|
137
|
+
if (message.type === 'control-point') {
|
|
138
|
+
channel.send({ type: 'control-state', id: message.id, control: readControl() });
|
|
139
|
+
continue;
|
|
150
140
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
if (message.type === 'progress') {
|
|
142
|
+
if (jsonBytes(message.event) > spec.policy.maxProgressBytes) {
|
|
143
|
+
await terminate();
|
|
144
|
+
emit('script_failed', { error: 'progress_too_large' });
|
|
145
|
+
return { ok: false, error: 'progress_too_large' };
|
|
146
|
+
}
|
|
147
|
+
emit('progress', message.event);
|
|
148
|
+
continue;
|
|
156
149
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
150
|
+
if (message.type === 'checkpoint') {
|
|
151
|
+
if (jsonBytes(message.state) > spec.policy.maxOutputBytes) {
|
|
152
|
+
await terminate();
|
|
153
|
+
emit('script_failed', { error: 'checkpoint_too_large' });
|
|
154
|
+
return { ok: false, error: 'checkpoint_too_large' };
|
|
155
|
+
}
|
|
156
|
+
const receipt = await rpc(Promise.resolve(emit('checkpoint', { name: message.name, state: message.state })));
|
|
157
|
+
if (receipt && receipt.type === 'timeout') {
|
|
158
|
+
await terminate();
|
|
159
|
+
emit('script_failed', { error: 'timeout' });
|
|
160
|
+
return { ok: false, error: 'timeout' };
|
|
161
|
+
}
|
|
162
|
+
if (receipt && receipt.ok === false) {
|
|
163
|
+
await terminate();
|
|
164
|
+
const error = receipt.error || 'checkpoint_not_persisted';
|
|
165
|
+
emit('script_failed', { error });
|
|
166
|
+
return { ok: false, error };
|
|
167
|
+
}
|
|
168
|
+
channel.send({ type: 'checkpoint-receipt', id: message.id, value: receipt, control: readControl() });
|
|
169
|
+
continue;
|
|
166
170
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
if (message.type === 'ask') {
|
|
172
|
+
const value = await rpc(agent.askAgent(message.request));
|
|
173
|
+
if (value && value.type === 'timeout') {
|
|
174
|
+
await terminate();
|
|
175
|
+
emit('script_failed', { error: 'timeout' });
|
|
176
|
+
return { ok: false, error: 'timeout' };
|
|
177
|
+
}
|
|
178
|
+
channel.send({ type: 'decision', id: message.id, value, control: readControl() });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'return') {
|
|
182
|
+
if (jsonBytes(message.result) > spec.policy.maxOutputBytes) {
|
|
183
|
+
await terminate();
|
|
184
|
+
emit('script_failed', { error: 'output_too_large' });
|
|
185
|
+
return { ok: false, error: 'output_too_large' };
|
|
186
|
+
}
|
|
187
|
+
child.stdin.end();
|
|
188
|
+
const ended = await Promise.race([finished, timedOut]);
|
|
189
|
+
if (ended && ended.type === 'timeout') {
|
|
190
|
+
await terminate();
|
|
191
|
+
} else {
|
|
192
|
+
await finished;
|
|
193
|
+
}
|
|
194
|
+
emit('script_completed', { result: message.result });
|
|
195
|
+
return { ok: true, result: message.result };
|
|
196
|
+
}
|
|
197
|
+
if (message.type === 'fail') {
|
|
172
198
|
await terminate();
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
const error = message.error === 'channel_closed' || message.error === 'stopped'
|
|
200
|
+
? 'child_crashed'
|
|
201
|
+
: (message.error || 'script_failed');
|
|
202
|
+
const captured = channel.diagnostics();
|
|
203
|
+
const diagnostics = { ...captured, ...(message.diagnostic ? { error: message.diagnostic } : {}) };
|
|
204
|
+
const extra = Object.keys(diagnostics).length ? { diagnostics } : {};
|
|
205
|
+
emit('script_failed', { error, ...extra });
|
|
206
|
+
return { ok: false, error, ...extra };
|
|
175
207
|
}
|
|
176
|
-
|
|
177
|
-
const ended = await Promise.race([finished, timedOut]);
|
|
178
|
-
if (ended && ended.type === 'timeout') {
|
|
208
|
+
if (message.type === 'exit') {
|
|
179
209
|
await terminate();
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
emit('script_failed', { error: 'child_crashed' });
|
|
211
|
+
return { ok: false, error: 'child_crashed' };
|
|
182
212
|
}
|
|
183
|
-
emit('script_completed', { result: message.result });
|
|
184
|
-
return { ok: true, result: message.result };
|
|
185
|
-
}
|
|
186
|
-
if (message.type === 'fail') {
|
|
187
|
-
await terminate();
|
|
188
|
-
const error = message.error === 'channel_closed' || message.error === 'stopped'
|
|
189
|
-
? 'child_crashed'
|
|
190
|
-
: (message.error || 'script_failed');
|
|
191
|
-
emit('script_failed', { error });
|
|
192
|
-
return { ok: false, error };
|
|
193
|
-
}
|
|
194
|
-
if (message.type === 'exit') {
|
|
195
213
|
await terminate();
|
|
196
214
|
emit('script_failed', { error: 'child_crashed' });
|
|
197
215
|
return { ok: false, error: 'child_crashed' };
|
|
198
216
|
}
|
|
199
|
-
await terminate();
|
|
200
|
-
emit('script_failed', { error: 'child_crashed' });
|
|
201
|
-
return { ok: false, error: 'child_crashed' };
|
|
202
|
-
}
|
|
203
217
|
} catch {
|
|
204
218
|
await terminate();
|
|
205
219
|
emit('script_failed', { error: 'child_crashed' });
|
|
206
220
|
return { ok: false, error: 'child_crashed' };
|
|
207
221
|
} finally {
|
|
208
222
|
if (timer) clearTimeout(timer);
|
|
223
|
+
await terminate();
|
|
224
|
+
if (directory) fs.rmSync(directory, { recursive: true, force: true });
|
|
209
225
|
}
|
|
210
226
|
}
|
|
211
227
|
|
|
@@ -35,7 +35,7 @@ function pythonCandidates({ pythonPath, env = process.env } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function probePython(executable, env) {
|
|
38
|
-
const options = { encoding: 'utf8', shell: false };
|
|
38
|
+
const options = { encoding: 'utf8', shell: false, timeout: 5000, killSignal: 'SIGKILL', maxBuffer: 16384 };
|
|
39
39
|
if (env) options.env = env;
|
|
40
40
|
const probe = spawnSync(executable, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], options);
|
|
41
41
|
if (probe.status !== 0 || !probe.stdout) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function boundedText(value, maxBytes) {
|
|
4
|
+
return Buffer.from(String(value)).subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function errorDiagnostic(error) {
|
|
8
|
+
const stack = typeof error?.stack === 'string' ? error.stack : '';
|
|
9
|
+
let frameCount = 0;
|
|
10
|
+
const limitedStack = stack.split('\n').filter(line => !/^\s*at /.test(line) || ++frameCount <= 20).join('\n');
|
|
11
|
+
const frame = stack.split('\n').find(line => /:(\d+):(\d+)\)?$/.test(line));
|
|
12
|
+
const location = frame?.match(/(?:at .*?\(|at )(.*):(\d+):(\d+)\)?$/);
|
|
13
|
+
const syntaxLocation = error?.name === 'SyntaxError' ? stack.split('\n')[0].match(/^(.*):(\d+)$/) : null;
|
|
14
|
+
return { type: boundedText(error?.name || 'Error', 128), message: boundedText(error?.message ?? error, 2048),
|
|
15
|
+
...(stack ? { stack: boundedText(limitedStack, 8192), stackTruncated: frameCount > 20 || Buffer.byteLength(limitedStack) > 8192 } : {}),
|
|
16
|
+
...(syntaxLocation ? { location: { file: boundedText(syntaxLocation[1], 1024), line: Number(syntaxLocation[2]) } }
|
|
17
|
+
: location ? { location: { file: boundedText(location[1], 1024), line: Number(location[2]), column: Number(location[3]) } } : {}),
|
|
18
|
+
...(typeof error?.valuePath === 'string' ? { valuePath: boundedText(error.valuePath, 1024) } : {}) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { boundedText, errorDiagnostic };
|
|
@@ -157,6 +157,7 @@ async function restoreUnknownOperation(args = {}) {
|
|
|
157
157
|
operationId,
|
|
158
158
|
status: terminal ? checkpoint.status : 'failed',
|
|
159
159
|
error: terminal ? checkpoint.error || null : 'runtime_lost',
|
|
160
|
+
...(checkpoint.diagnostics ? { diagnostics: checkpoint.diagnostics } : {}),
|
|
160
161
|
persisted: true,
|
|
161
162
|
restored: true,
|
|
162
163
|
resumeMode: gate.ok ? 'checkpoint' : 'none',
|
|
@@ -229,7 +229,7 @@ async function mutationResult(command, args, options, ctx) {
|
|
|
229
229
|
const actionId = options.dispatchActionId || mutationActionId(ctx.executionId, ctx.callId);
|
|
230
230
|
const bound = dispatchArgs(args, actionId);
|
|
231
231
|
try {
|
|
232
|
-
|
|
232
|
+
const execute = async () => {
|
|
233
233
|
try { return await actionOnce(command, bound, options, {
|
|
234
234
|
actions: ctx.actions,
|
|
235
235
|
target: ctx.target,
|
|
@@ -238,7 +238,8 @@ async function mutationResult(command, args, options, ctx) {
|
|
|
238
238
|
actionId,
|
|
239
239
|
onAction: ctx.onAction,
|
|
240
240
|
}); } finally { ctx.onSettled(); }
|
|
241
|
-
}
|
|
241
|
+
};
|
|
242
|
+
return command === 'executor-prepare' ? await execute() : await admitExecutionMutation(ctx.target, ctx.mutationLease, execute);
|
|
242
243
|
} catch (error) {
|
|
243
244
|
return unavailableEnvelope({ command, error: error.code || error.message, executionId: ctx.executionId, callId: ctx.callId });
|
|
244
245
|
}
|
package/bin/script/script-sdk.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const readline = require('node:readline');
|
|
5
|
+
const { boundedText, errorDiagnostic } = require('./script-diagnostics');
|
|
5
6
|
|
|
6
|
-
const artifactPath = process.argv[2];
|
|
7
|
+
const artifactPath = fs.realpathSync(process.argv[2]);
|
|
7
8
|
const replies = new Map();
|
|
8
9
|
const controlWaiters = [];
|
|
9
10
|
let nextId = 0;
|
|
@@ -37,6 +38,7 @@ function waitReply(id) {
|
|
|
37
38
|
rl.on('line', (line) => {
|
|
38
39
|
if (!line) return;
|
|
39
40
|
let message;
|
|
41
|
+
const parseStarted = performance.now();
|
|
40
42
|
try {
|
|
41
43
|
message = JSON.parse(line);
|
|
42
44
|
} catch {
|
|
@@ -44,10 +46,21 @@ rl.on('line', (line) => {
|
|
|
44
46
|
process.exit(1);
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
49
|
+
const inputParseMs = performance.now() - parseStarted;
|
|
47
50
|
if (message.type === 'start') {
|
|
48
51
|
applyControl(message.control);
|
|
49
|
-
run(message).catch((error) => {
|
|
50
|
-
|
|
52
|
+
run(message, inputParseMs).catch((error) => {
|
|
53
|
+
const diagnostic = errorDiagnostic(error);
|
|
54
|
+
if (message.sourceName && message.sourceName !== artifactPath) {
|
|
55
|
+
if (diagnostic.stack) {
|
|
56
|
+
const mapped = diagnostic.stack.replaceAll(artifactPath, message.sourceName);
|
|
57
|
+
diagnostic.stack = boundedText(mapped, 8192);
|
|
58
|
+
diagnostic.stackTruncated ||= Buffer.byteLength(mapped) > 8192;
|
|
59
|
+
}
|
|
60
|
+
if (diagnostic.location?.file === artifactPath) diagnostic.location.file = boundedText(message.sourceName, 1024);
|
|
61
|
+
}
|
|
62
|
+
send({ type: 'fail', error: message.extraction ? (error.code || 'extraction_failed') : diagnostic.message,
|
|
63
|
+
message: diagnostic.message, diagnostic });
|
|
51
64
|
process.exit(1);
|
|
52
65
|
});
|
|
53
66
|
return;
|
|
@@ -92,7 +105,29 @@ function holdIfPaused() {
|
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
async function run(start) {
|
|
108
|
+
async function run(start, inputParseMs) {
|
|
109
|
+
if (start.extraction) {
|
|
110
|
+
const executionStarted = performance.now();
|
|
111
|
+
let result;
|
|
112
|
+
if (start.extraction.mode === 'regex') result = require('../extraction/regex').extractRegex(start.inputs.response, start.extraction);
|
|
113
|
+
else {
|
|
114
|
+
const Module = require('node:module');
|
|
115
|
+
const path = require('node:path');
|
|
116
|
+
const filename = path.resolve(start.sourceName || artifactPath);
|
|
117
|
+
const loaded = new Module(filename, module);
|
|
118
|
+
loaded.filename = filename;
|
|
119
|
+
loaded.paths = Module._nodeModulePaths(path.dirname(filename));
|
|
120
|
+
loaded._compile(fs.readFileSync(artifactPath, 'utf8'), filename);
|
|
121
|
+
const main = typeof loaded.exports === 'function' ? loaded.exports : loaded.exports.main;
|
|
122
|
+
if (typeof main !== 'function') throw new TypeError('Extraction source must export main(ctx).');
|
|
123
|
+
result = await main({ inputs: start.inputs });
|
|
124
|
+
}
|
|
125
|
+
require('../extraction/json-value').validateJsonValue(result);
|
|
126
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 256 * 1024) throw Object.assign(new Error('Extraction result exceeds 256 KiB.'), { code: 'extraction_output_too_large' });
|
|
127
|
+
send({ type: 'return', result, timings: { inputParseMs, executionMs: performance.now() - executionStarted } });
|
|
128
|
+
process.exit(0);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
96
131
|
const loaded = require(artifactPath);
|
|
97
132
|
const entry = start.entrypoint || 'main';
|
|
98
133
|
const main = typeof loaded === 'function' && entry === 'main' ? loaded : loaded[entry];
|
package/bin/script/script-sdk.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
|
+
import math
|
|
5
|
+
import traceback
|
|
6
|
+
import time
|
|
7
|
+
from types import SimpleNamespace
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
_real_stdout = sys.stdout
|
|
11
|
+
_input_parse_ms = 0
|
|
7
12
|
HELD = {
|
|
8
13
|
"pause_requested",
|
|
9
14
|
"paused_manual",
|
|
@@ -20,10 +25,14 @@ def send(message):
|
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def read_message():
|
|
28
|
+
global _input_parse_ms
|
|
23
29
|
line = sys.stdin.readline()
|
|
24
30
|
if not line:
|
|
25
31
|
return None
|
|
26
|
-
|
|
32
|
+
started = time.perf_counter()
|
|
33
|
+
message = json.loads(line)
|
|
34
|
+
_input_parse_ms = (time.perf_counter() - started) * 1000
|
|
35
|
+
return message
|
|
27
36
|
|
|
28
37
|
|
|
29
38
|
class HostContext:
|
|
@@ -120,22 +129,74 @@ class HostContext:
|
|
|
120
129
|
return checkpoint["state"]
|
|
121
130
|
|
|
122
131
|
|
|
123
|
-
def load_namespace(path):
|
|
132
|
+
def load_namespace(path, source_name=None):
|
|
124
133
|
namespace = {}
|
|
125
134
|
with open(path, "r", encoding="utf-8") as handle:
|
|
126
|
-
exec(handle.read(), namespace, namespace)
|
|
135
|
+
exec(compile(handle.read(), source_name or path, "exec"), namespace, namespace)
|
|
127
136
|
return namespace
|
|
128
137
|
|
|
129
138
|
|
|
139
|
+
def validate_json(value, location="", parents=None):
|
|
140
|
+
parents = set() if parents is None else parents
|
|
141
|
+
|
|
142
|
+
def invalid(reason):
|
|
143
|
+
error = TypeError(f"{location or '/'}: {reason}")
|
|
144
|
+
error.code = "extraction_type_error"
|
|
145
|
+
error.value_path = location
|
|
146
|
+
raise error
|
|
147
|
+
|
|
148
|
+
if value is None or isinstance(value, (str, bool)):
|
|
149
|
+
return
|
|
150
|
+
if isinstance(value, (int, float)):
|
|
151
|
+
if (isinstance(value, int) and abs(value) > 9007199254740991) or (isinstance(value, float) and
|
|
152
|
+
(not math.isfinite(value) or value.is_integer() and abs(value) > 9007199254740991)):
|
|
153
|
+
invalid("expected a finite number in the safe integer range")
|
|
154
|
+
return
|
|
155
|
+
if type(value) not in (list, dict):
|
|
156
|
+
invalid(f"unsupported JSON type: {type(value).__name__}")
|
|
157
|
+
if id(value) in parents:
|
|
158
|
+
invalid("cyclic value")
|
|
159
|
+
parents.add(id(value))
|
|
160
|
+
entries = enumerate(value) if isinstance(value, list) else value.items()
|
|
161
|
+
for key, item in entries:
|
|
162
|
+
if isinstance(value, dict) and not isinstance(key, str):
|
|
163
|
+
invalid("object keys must be strings")
|
|
164
|
+
suffix = str(key).replace("~", "~0").replace("/", "~1")
|
|
165
|
+
validate_json(item, f"{location}/{suffix}", parents)
|
|
166
|
+
parents.remove(id(value))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def bounded(value, size):
|
|
170
|
+
return str(value).encode("utf-8", errors="replace")[:size].decode("utf-8", errors="ignore")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def diagnostic(error):
|
|
174
|
+
frames = traceback.extract_tb(error.__traceback__)
|
|
175
|
+
stack = "Traceback (most recent call last):\n" + "".join(traceback.format_list(frames[-20:])) + "".join(traceback.format_exception_only(type(error), error))
|
|
176
|
+
result = {"type": type(error).__name__, "message": bounded(error, 2048),
|
|
177
|
+
"stack": bounded(stack, 8192), "stackTruncated": len(frames) > 20 or len(stack.encode("utf-8")) > 8192}
|
|
178
|
+
if isinstance(error, SyntaxError) and error.filename:
|
|
179
|
+
result["location"] = {"file": bounded(error.filename, 1024), "line": error.lineno, "column": error.offset}
|
|
180
|
+
elif frames:
|
|
181
|
+
frame = frames[-1]
|
|
182
|
+
result["location"] = {"file": bounded(frame.filename, 1024), "line": frame.lineno}
|
|
183
|
+
if hasattr(error, "value_path"):
|
|
184
|
+
result["valuePath"] = bounded(error.value_path, 1024)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
|
|
130
188
|
def main():
|
|
131
189
|
artifact_path = sys.argv[1]
|
|
132
190
|
send({"type": "ready"})
|
|
133
191
|
start = read_message()
|
|
134
192
|
sys.stdout = sys.stderr
|
|
135
|
-
|
|
193
|
+
extraction = start.get("extraction") is not None
|
|
194
|
+
ctx = SimpleNamespace(inputs=start["inputs"]) if extraction else HostContext(start.get("inputs") or {}, start.get("control") or {"status": "running", "pauseReason": None})
|
|
136
195
|
entry = start.get("entrypoint") or "main"
|
|
137
196
|
try:
|
|
138
|
-
|
|
197
|
+
execution_started = time.perf_counter()
|
|
198
|
+
input_parse_ms = _input_parse_ms
|
|
199
|
+
namespace = load_namespace(artifact_path, start.get("sourceName"))
|
|
139
200
|
loaded = namespace.get(entry)
|
|
140
201
|
if not callable(loaded):
|
|
141
202
|
send({"type": "fail", "error": "unsupported_entrypoint"})
|
|
@@ -143,9 +204,20 @@ def main():
|
|
|
143
204
|
result = loaded(ctx)
|
|
144
205
|
if asyncio.iscoroutine(result):
|
|
145
206
|
result = asyncio.run(result)
|
|
146
|
-
|
|
207
|
+
if extraction:
|
|
208
|
+
validate_json(result)
|
|
209
|
+
if len(json.dumps(result, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")) > 256 * 1024:
|
|
210
|
+
error = ValueError("Extraction result exceeds 256 KiB.")
|
|
211
|
+
error.code = "extraction_output_too_large"
|
|
212
|
+
raise error
|
|
213
|
+
frame = {"type": "return", "result": result}
|
|
214
|
+
if extraction:
|
|
215
|
+
frame["timings"] = {"inputParseMs": input_parse_ms, "executionMs": (time.perf_counter() - execution_started) * 1000}
|
|
216
|
+
send(frame)
|
|
147
217
|
except Exception as error:
|
|
148
|
-
|
|
218
|
+
detail = diagnostic(error)
|
|
219
|
+
send({"type": "fail", "error": getattr(error, "code", "extraction_failed") if extraction else detail["message"],
|
|
220
|
+
"message": detail["message"], "diagnostic": detail})
|
|
149
221
|
sys.exit(1)
|
|
150
222
|
|
|
151
223
|
|