@moxxy/sdk 0.15.1 → 0.16.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/dist/compactor.d.ts +1 -1
- package/dist/compactor.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/mode/abort-backoff.d.ts +23 -0
- package/dist/mode/abort-backoff.d.ts.map +1 -0
- package/dist/mode/abort-backoff.js +53 -0
- package/dist/mode/abort-backoff.js.map +1 -0
- package/dist/mode/project-messages.d.ts.map +1 -1
- package/dist/mode/project-messages.js +38 -4
- package/dist/mode/project-messages.js.map +1 -1
- package/dist/mode-helpers.d.ts +1 -0
- package/dist/mode-helpers.d.ts.map +1 -1
- package/dist/mode-helpers.js +1 -0
- package/dist/mode-helpers.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/requirements.d.ts +18 -3
- package/dist/requirements.d.ts.map +1 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +0 -1
- package/dist/server.js.map +1 -1
- package/dist/tool-display.d.ts.map +1 -1
- package/dist/tool-display.js +23 -4
- package/dist/tool-display.js.map +1 -1
- package/dist/tunnel.d.ts +21 -34
- package/dist/tunnel.d.ts.map +1 -1
- package/dist/tunnel.js +11 -172
- package/dist/tunnel.js.map +1 -1
- package/dist/view-renderer.d.ts +6 -0
- package/dist/view-renderer.d.ts.map +1 -1
- package/dist/view-renderer.js +17 -3
- package/dist/view-renderer.js.map +1 -1
- package/package.json +1 -1
- package/src/compactor.ts +6 -1
- package/src/index.ts +7 -11
- package/src/loop-helpers.test.ts +72 -0
- package/src/mode/abort-backoff.test.ts +64 -0
- package/src/mode/abort-backoff.ts +53 -0
- package/src/mode/project-messages.ts +35 -4
- package/src/mode-helpers.ts +1 -0
- package/src/package-root.test.ts +0 -2
- package/src/plugin.ts +1 -1
- package/src/requirements.ts +15 -3
- package/src/server.ts +0 -1
- package/src/tool-display.test.ts +24 -0
- package/src/tool-display.ts +22 -6
- package/src/tunnel.ts +19 -198
- package/src/view-renderer.test.ts +12 -0
- package/src/view-renderer.ts +16 -2
- package/src/tunnel.test.ts +0 -99
package/dist/tunnel.js
CHANGED
|
@@ -1,174 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A swappable way to expose a locally-bound surface (the web channel
|
|
3
|
-
* user over the public internet — so an
|
|
4
|
-
* a URL they can open. One provider is
|
|
5
|
-
* plugins, like every other block); core
|
|
6
|
-
* `getActive()` is non-null.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// thing: spawn a CLI, watch its stdout/stderr for the assigned public URL,
|
|
14
|
-
// resolve once it's seen (or reject on timeout/exit/error), and guarantee the
|
|
15
|
-
// child is killed on close *and* on process exit (Node does not reap children).
|
|
16
|
-
// `spawnCliTunnel` is the single implementation they all share.
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
/**
|
|
19
|
-
* Guarantees spawned tunnel children (cloudflared/ngrok/…) never orphan or
|
|
20
|
-
* leak. Node does NOT kill child processes when the parent exits, so we track
|
|
21
|
-
* every live child and kill any survivors on process teardown — in addition to
|
|
22
|
-
* the explicit `close()` path. Each tracked child is also force-killed
|
|
23
|
-
* (SIGKILL) if it ignores SIGTERM, so a wedged tunnel can't drain
|
|
24
|
-
* memory/handles.
|
|
25
|
-
*/
|
|
26
|
-
const liveChildren = new Set();
|
|
27
|
-
let exitHookInstalled = false;
|
|
28
|
-
function killChild(child) {
|
|
29
|
-
if (child.exitCode != null || child.signalCode != null)
|
|
30
|
-
return;
|
|
31
|
-
try {
|
|
32
|
-
child.kill('SIGTERM');
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
/* already gone */
|
|
36
|
-
}
|
|
37
|
-
// Escalate if it doesn't exit promptly. unref so this timer never holds the
|
|
38
|
-
// event loop open on its own.
|
|
39
|
-
const t = setTimeout(() => {
|
|
40
|
-
try {
|
|
41
|
-
child.kill('SIGKILL');
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
/* gone */
|
|
45
|
-
}
|
|
46
|
-
}, 2000);
|
|
47
|
-
t.unref?.();
|
|
48
|
-
child.once('exit', () => clearTimeout(t));
|
|
49
|
-
}
|
|
50
|
-
function ensureExitHook() {
|
|
51
|
-
if (exitHookInstalled)
|
|
52
|
-
return;
|
|
53
|
-
exitHookInstalled = true;
|
|
54
|
-
const killAll = () => {
|
|
55
|
-
for (const child of liveChildren) {
|
|
56
|
-
try {
|
|
57
|
-
child.kill('SIGKILL'); // process is exiting; be decisive, no async escalation
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
/* gone */
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
liveChildren.clear();
|
|
64
|
-
};
|
|
65
|
-
process.once('exit', killAll);
|
|
66
|
-
// Registering a SIGINT/SIGTERM listener suppresses Node's default
|
|
67
|
-
// terminate-on-signal behavior, so an entrypoint that spawns a tunnel but
|
|
68
|
-
// installs no exit handler of its own would swallow the first Ctrl-C and hang
|
|
69
|
-
// with the process still alive. Re-raise the signal after cleanup so the
|
|
70
|
-
// default termination still fires. `process.once` removes the listener before
|
|
71
|
-
// it runs, so re-raising hits Node's default disposition (terminate).
|
|
72
|
-
const onSignal = (sig) => () => {
|
|
73
|
-
killAll();
|
|
74
|
-
process.kill(process.pid, sig);
|
|
75
|
-
};
|
|
76
|
-
process.once('SIGINT', onSignal('SIGINT'));
|
|
77
|
-
process.once('SIGTERM', onSignal('SIGTERM'));
|
|
78
|
-
}
|
|
79
|
-
/** Track a spawned child; returns an `untrack()` that also kills it cleanly. */
|
|
80
|
-
function trackChild(child) {
|
|
81
|
-
ensureExitHook();
|
|
82
|
-
liveChildren.add(child);
|
|
83
|
-
child.once('exit', () => liveChildren.delete(child));
|
|
84
|
-
return () => new Promise((resolve) => {
|
|
85
|
-
liveChildren.delete(child);
|
|
86
|
-
if (child.exitCode != null || child.signalCode != null)
|
|
87
|
-
return resolve();
|
|
88
|
-
child.once('exit', () => resolve());
|
|
89
|
-
killChild(child);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const DEFAULT_TUNNEL_URL_TIMEOUT_MS = 30_000;
|
|
93
|
-
/**
|
|
94
|
-
* Spawn a CLI tunnel, parse the assigned public URL out of its output, and
|
|
95
|
-
* resolve a {@link CliTunnelHandle}. The child is tracked so it is killed on
|
|
96
|
-
* `close()`, on tunnel-switch, and on process exit (no orphans). Rejects on
|
|
97
|
-
* timeout, spawn error, or premature exit (the child is killed in every reject
|
|
98
|
-
* path). This is the single spawn-and-parse implementation behind the
|
|
99
|
-
* cloudflared/ngrok tunnel providers and the webhooks tunnel.
|
|
100
|
-
*/
|
|
101
|
-
export function spawnCliTunnel(opts) {
|
|
102
|
-
const { cmd, args, urlRegex } = opts;
|
|
103
|
-
const name = opts.name ?? cmd;
|
|
104
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TUNNEL_URL_TIMEOUT_MS;
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
let child;
|
|
107
|
-
try {
|
|
108
|
-
child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
reject(new Error(`failed to spawn ${name} — is it installed? (${err instanceof Error ? err.message : String(err)})`));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const untrack = trackChild(child);
|
|
115
|
-
let settled = false;
|
|
116
|
-
const timer = setTimeout(() => {
|
|
117
|
-
if (settled)
|
|
118
|
-
return;
|
|
119
|
-
settled = true;
|
|
120
|
-
void untrack();
|
|
121
|
-
reject(new Error(`${name}: timed out after ${timeoutMs}ms waiting for the tunnel URL`));
|
|
122
|
-
}, timeoutMs);
|
|
123
|
-
timer.unref?.();
|
|
124
|
-
// 'data' events are not line-buffered: the assigned URL can straddle two
|
|
125
|
-
// chunk boundaries (large URL, fragmented flush). Match against a rolling
|
|
126
|
-
// accumulation rather than each lone chunk, capped so a chatty tunnel that
|
|
127
|
-
// never prints a URL can't grow this unboundedly while we wait.
|
|
128
|
-
const MAX_BUF = 8192;
|
|
129
|
-
let acc = '';
|
|
130
|
-
const onData = (buf) => {
|
|
131
|
-
if (settled)
|
|
132
|
-
return; // drain quietly once resolved so the pipe never fills
|
|
133
|
-
acc += buf.toString('utf8');
|
|
134
|
-
if (acc.length > MAX_BUF)
|
|
135
|
-
acc = acc.slice(acc.length - MAX_BUF);
|
|
136
|
-
const url = urlRegex.exec(acc)?.[0] ?? null;
|
|
137
|
-
if (!url)
|
|
138
|
-
return;
|
|
139
|
-
settled = true;
|
|
140
|
-
acc = '';
|
|
141
|
-
clearTimeout(timer);
|
|
142
|
-
resolve({ url, pid: child.pid ?? -1, close: untrack });
|
|
143
|
-
};
|
|
144
|
-
child.stdout?.on('data', onData);
|
|
145
|
-
child.stderr?.on('data', onData);
|
|
146
|
-
child.on('error', (err) => {
|
|
147
|
-
if (settled)
|
|
148
|
-
return;
|
|
149
|
-
settled = true;
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
void untrack();
|
|
152
|
-
reject(err);
|
|
153
|
-
});
|
|
154
|
-
child.on('exit', (code) => {
|
|
155
|
-
if (settled)
|
|
156
|
-
return;
|
|
157
|
-
settled = true;
|
|
158
|
-
clearTimeout(timer);
|
|
159
|
-
reject(new Error(`${name} exited (code ${code ?? 'null'}) before emitting a URL`));
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Probe whether a tunnel CLI is installed and runnable (`<cmd> --version`
|
|
165
|
-
* exits 0). Used as the `isAvailable()` gate by CLI-backed tunnel providers.
|
|
166
|
-
*/
|
|
167
|
-
export function isCliTunnelAvailable(cmd) {
|
|
168
|
-
return new Promise((resolve) => {
|
|
169
|
-
const child = spawn(cmd, ['--version'], { stdio: 'ignore' });
|
|
170
|
-
child.once('error', () => resolve(false));
|
|
171
|
-
child.once('exit', (code) => resolve(code === 0));
|
|
172
|
-
});
|
|
173
|
-
}
|
|
2
|
+
* A swappable way to expose a locally-bound surface (the web channel, the mobile
|
|
3
|
+
* bridge, the webhooks listener) to the user over the public internet — so an
|
|
4
|
+
* agent on Telegram/TUI can hand the user a URL they can open. One provider is
|
|
5
|
+
* active per session (registered via plugins, like every other block); core
|
|
6
|
+
* seeds a `localhost` no-op provider so `getActive()` is non-null.
|
|
7
|
+
*
|
|
8
|
+
* The shipped provider is `@moxxy/plugin-tunnel-proxy` (the self-hosted proxy
|
|
9
|
+
* relay). The old cloudflared/ngrok subprocess providers — and the
|
|
10
|
+
* `spawnCliTunnel` helper that backed them — were removed.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
174
13
|
//# sourceMappingURL=tunnel.js.map
|
package/dist/tunnel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
package/dist/view-renderer.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export declare const DEFAULT_VIEW_TAGS: ReadonlyArray<ViewTagSpec>;
|
|
|
113
113
|
* Count element + text nodes in a view tree (the `nodeCount` a `present_view`
|
|
114
114
|
* tool reports). Single source of truth: core's parser and the plugin-view tool
|
|
115
115
|
* both import this rather than re-implementing the recursion.
|
|
116
|
+
*
|
|
117
|
+
* Iterative (explicit stack) rather than recursive: a deeply nested
|
|
118
|
+
* hand-built/corrupt AST would blow the call stack with a `RangeError`, and this
|
|
119
|
+
* runs on untrusted agent-authored input. Each node counts as 1; an element's
|
|
120
|
+
* children are pushed for later counting. Order-independent (we only sum), so
|
|
121
|
+
* the count is byte-identical to the old recursion for any normal tree.
|
|
116
122
|
*/
|
|
117
123
|
export declare function countNodes(node: ViewNode): number;
|
|
118
124
|
//# sourceMappingURL=view-renderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-renderer.d.ts","sourceRoot":"","sources":["../src/view-renderer.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"view-renderer.d.ts","sourceRoot":"","sources":["../src/view-renderer.ts"],"names":[],"mappings":"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"}
|
package/dist/view-renderer.js
CHANGED
|
@@ -104,10 +104,24 @@ export const DEFAULT_VIEW_TAGS = [...VIEW_PRIMITIVES, ...VIEW_COMPONENTS];
|
|
|
104
104
|
* Count element + text nodes in a view tree (the `nodeCount` a `present_view`
|
|
105
105
|
* tool reports). Single source of truth: core's parser and the plugin-view tool
|
|
106
106
|
* both import this rather than re-implementing the recursion.
|
|
107
|
+
*
|
|
108
|
+
* Iterative (explicit stack) rather than recursive: a deeply nested
|
|
109
|
+
* hand-built/corrupt AST would blow the call stack with a `RangeError`, and this
|
|
110
|
+
* runs on untrusted agent-authored input. Each node counts as 1; an element's
|
|
111
|
+
* children are pushed for later counting. Order-independent (we only sum), so
|
|
112
|
+
* the count is byte-identical to the old recursion for any normal tree.
|
|
107
113
|
*/
|
|
108
114
|
export function countNodes(node) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
let count = 0;
|
|
116
|
+
const stack = [node];
|
|
117
|
+
while (stack.length > 0) {
|
|
118
|
+
const current = stack.pop();
|
|
119
|
+
count += 1;
|
|
120
|
+
if (current.kind === 'element') {
|
|
121
|
+
for (const child of current.children)
|
|
122
|
+
stack.push(child);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return count;
|
|
112
126
|
}
|
|
113
127
|
//# sourceMappingURL=view-renderer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-renderer.js","sourceRoot":"","sources":["../src/view-renderer.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"view-renderer.js","sourceRoot":"","sources":["../src/view-renderer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/compactor.ts
CHANGED
|
@@ -26,5 +26,10 @@ export interface CompactContext {
|
|
|
26
26
|
export interface CompactorDef {
|
|
27
27
|
readonly name: string;
|
|
28
28
|
shouldCompact(log: EventLogReader, budget: TokenBudget): boolean;
|
|
29
|
-
|
|
29
|
+
// `ctx` is optional in the public contract: the dispatcher
|
|
30
|
+
// (`runCompactionIfNeeded`) always passes one, but impls treat it as optional
|
|
31
|
+
// (degrading gracefully when provider/model are absent), so a hand-rolled
|
|
32
|
+
// caller / test invoking `compact(events)` is valid and must not be a
|
|
33
|
+
// compile error.
|
|
34
|
+
compact(events: ReadonlyArray<MoxxyEvent>, ctx?: CompactContext): Promise<Omit<CompactionEvent, keyof import('./events.js').EventBase> & { ts?: number }>;
|
|
30
35
|
}
|
package/src/index.ts
CHANGED
|
@@ -175,17 +175,11 @@ export type {
|
|
|
175
175
|
ViewRendererDef,
|
|
176
176
|
} from './view-renderer.js';
|
|
177
177
|
export { VIEW_PRIMITIVES, VIEW_COMPONENTS, DEFAULT_VIEW_TAGS, isSafeViewUrl, countNodes } from './view-renderer.js';
|
|
178
|
-
export type {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
CliTunnelHandle,
|
|
184
|
-
} from './tunnel.js';
|
|
185
|
-
// Node-runtime helpers (spawnCliTunnel/isCliTunnelAvailable, writeFileAtomic*,
|
|
186
|
-
// moxxyHome/moxxyPath, readRequestBody/bearerTokenMatches, channel-auth) are
|
|
187
|
-
// exported from the './server' subpath, NOT the main barrel — they statically
|
|
188
|
-
// reach node:* builtins and would break a browser/RN bundle. See ./server.ts.
|
|
178
|
+
export type { TunnelProviderDef, TunnelHandle, TunnelOpenOptions } from './tunnel.js';
|
|
179
|
+
// Node-runtime helpers (writeFileAtomic*, moxxyHome/moxxyPath,
|
|
180
|
+
// readRequestBody/bearerTokenMatches, channel-auth) are exported from the
|
|
181
|
+
// './server' subpath, NOT the main barrel — they statically reach node:*
|
|
182
|
+
// builtins and would break a browser/RN bundle. See ./server.ts.
|
|
189
183
|
export { isRetryableError, toFriendlyError, zodToJsonSchema, estimateTextTokens, type StopReason } from './provider-utils.js';
|
|
190
184
|
export type { WriteFileAtomicOptions } from './fs-utils.js';
|
|
191
185
|
export { createMutex, type Mutex } from './mutex.js';
|
|
@@ -226,6 +220,8 @@ export {
|
|
|
226
220
|
buildSystemPromptWithSkills,
|
|
227
221
|
createStuckLoopDetector,
|
|
228
222
|
stableHash,
|
|
223
|
+
sleepWithAbort,
|
|
224
|
+
nextBackoffMs,
|
|
229
225
|
type CollectedToolUse,
|
|
230
226
|
type StreamResult,
|
|
231
227
|
type ProjectMessagesOptions,
|
package/src/loop-helpers.test.ts
CHANGED
|
@@ -191,6 +191,78 @@ describe('projectMessagesFromLog', () => {
|
|
|
191
191
|
expect(block.content).not.toContain('before');
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
it('projects an image-shaped tool result as an image block, not stringified text', () => {
|
|
196
|
+
// A screenshot/clipboard tool returns { mediaType, base64 }. The model must
|
|
197
|
+
// SEE the pixels (image block), not a giant base64 string JSON-stringified
|
|
198
|
+
// into the tool_result text.
|
|
199
|
+
const log = reader([
|
|
200
|
+
event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'screenshot' }),
|
|
201
|
+
event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: 'c1', name: 'screenshot', input: {} }),
|
|
202
|
+
event(2, {
|
|
203
|
+
type: 'tool_result',
|
|
204
|
+
turnId: t1,
|
|
205
|
+
source: 'tool',
|
|
206
|
+
callId: 'c1',
|
|
207
|
+
ok: true,
|
|
208
|
+
output: { mediaType: 'image/png', base64: 'AAAABBBB' },
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const messages = projectMessagesFromLog({ log });
|
|
213
|
+
const toolResult = messages.find((m) => m.role === 'tool_result')!;
|
|
214
|
+
// tool_result block (satisfies the tool_use pairing) + the image block.
|
|
215
|
+
const imageBlock = toolResult.content.find((b) => b.type === 'image');
|
|
216
|
+
expect(imageBlock).toEqual({ type: 'image', mediaType: 'image/png', data: 'AAAABBBB' });
|
|
217
|
+
const resultBlock = toolResult.content.find((b) => b.type === 'tool_result');
|
|
218
|
+
expect(resultBlock).toMatchObject({ type: 'tool_result', toolUseId: 'c1' });
|
|
219
|
+
// The base64 bytes must NOT be stringified into the tool_result text.
|
|
220
|
+
if (resultBlock && resultBlock.type === 'tool_result') {
|
|
221
|
+
expect(resultBlock.content).not.toContain('AAAABBBB');
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('accepts the provider-native image shape { type:image, mediaType, data }', () => {
|
|
226
|
+
const log = reader([
|
|
227
|
+
event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'grab' }),
|
|
228
|
+
event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: 'c1', name: 'grab', input: {} }),
|
|
229
|
+
event(2, {
|
|
230
|
+
type: 'tool_result',
|
|
231
|
+
turnId: t1,
|
|
232
|
+
source: 'tool',
|
|
233
|
+
callId: 'c1',
|
|
234
|
+
ok: true,
|
|
235
|
+
output: { type: 'image', mediaType: 'image/jpeg', data: 'ZZZZ' },
|
|
236
|
+
}),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const messages = projectMessagesFromLog({ log });
|
|
240
|
+
const toolResult = messages.find((m) => m.role === 'tool_result')!;
|
|
241
|
+
expect(toolResult.content.find((b) => b.type === 'image')).toEqual({
|
|
242
|
+
type: 'image',
|
|
243
|
+
mediaType: 'image/jpeg',
|
|
244
|
+
data: 'ZZZZ',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not treat a failed/error tool result as an image', () => {
|
|
249
|
+
const log = reader([
|
|
250
|
+
event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'grab' }),
|
|
251
|
+
event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: 'c1', name: 'grab', input: {} }),
|
|
252
|
+
event(2, {
|
|
253
|
+
type: 'tool_result',
|
|
254
|
+
turnId: t1,
|
|
255
|
+
source: 'tool',
|
|
256
|
+
callId: 'c1',
|
|
257
|
+
ok: false,
|
|
258
|
+
output: { mediaType: 'image/png', base64: 'AAAA' },
|
|
259
|
+
}),
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const messages = projectMessagesFromLog({ log });
|
|
263
|
+
const toolResult = messages.find((m) => m.role === 'tool_result')!;
|
|
264
|
+
expect(toolResult.content.some((b) => b.type === 'image')).toBe(false);
|
|
265
|
+
});
|
|
194
266
|
});
|
|
195
267
|
|
|
196
268
|
function reader(events: ReadonlyArray<MoxxyEvent>): EventLogReader {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { nextBackoffMs, sleepWithAbort } from './abort-backoff.js';
|
|
3
|
+
|
|
4
|
+
describe('nextBackoffMs', () => {
|
|
5
|
+
it('grows exponentially from baseMs (1-based attempt)', () => {
|
|
6
|
+
expect(nextBackoffMs(1, 500)).toBe(500);
|
|
7
|
+
expect(nextBackoffMs(2, 500)).toBe(1000);
|
|
8
|
+
expect(nextBackoffMs(3, 500)).toBe(2000);
|
|
9
|
+
expect(nextBackoffMs(4, 500)).toBe(4000);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('treats attempt <= 1 as baseMs', () => {
|
|
13
|
+
expect(nextBackoffMs(0, 500)).toBe(500);
|
|
14
|
+
expect(nextBackoffMs(-3, 500)).toBe(500);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('caps at maxMs (default 30_000)', () => {
|
|
18
|
+
expect(nextBackoffMs(20, 500)).toBe(30_000);
|
|
19
|
+
expect(nextBackoffMs(20, 500, 5_000)).toBe(5_000);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('sleepWithAbort', () => {
|
|
24
|
+
it('resolves after the delay', async () => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
try {
|
|
27
|
+
const p = sleepWithAbort(1000);
|
|
28
|
+
vi.advanceTimersByTime(1000);
|
|
29
|
+
await expect(p).resolves.toBeUndefined();
|
|
30
|
+
} finally {
|
|
31
|
+
vi.useRealTimers();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rejects immediately when the signal is already aborted', async () => {
|
|
36
|
+
const ac = new AbortController();
|
|
37
|
+
ac.abort();
|
|
38
|
+
await expect(sleepWithAbort(1000, ac.signal)).rejects.toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects on abort mid-sleep and leaves no abort listener leaked', async () => {
|
|
42
|
+
const ac = new AbortController();
|
|
43
|
+
const add = vi.spyOn(ac.signal, 'addEventListener');
|
|
44
|
+
const p = sleepWithAbort(10_000, ac.signal);
|
|
45
|
+
ac.abort();
|
|
46
|
+
await expect(p).rejects.toBeDefined();
|
|
47
|
+
// The once-listener is consumed by the abort; nothing keeps it attached.
|
|
48
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('removes the abort listener when it resolves normally (no leak)', async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
try {
|
|
54
|
+
const ac = new AbortController();
|
|
55
|
+
const remove = vi.spyOn(ac.signal, 'removeEventListener');
|
|
56
|
+
const p = sleepWithAbort(50, ac.signal);
|
|
57
|
+
vi.advanceTimersByTime(50);
|
|
58
|
+
await p;
|
|
59
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
60
|
+
} finally {
|
|
61
|
+
vi.useRealTimers();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry primitives for loop strategies (mode-default / mode-goal).
|
|
3
|
+
*
|
|
4
|
+
* Both modes back off between retryable provider failures with an identical
|
|
5
|
+
* exponential schedule and an abort-aware sleep. The logic was duplicated in
|
|
6
|
+
* each mode's loop; these are the single source of truth they now import.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sleep `ms` milliseconds, settling early if `signal` aborts. Rejects with the
|
|
11
|
+
* signal's abort reason (an `AbortError`-style `DOMException` when none) on
|
|
12
|
+
* abort so a back-off never silently outlives a cancelled turn, and crucially
|
|
13
|
+
* NEVER leaks the abort listener or the timer in any settle path (resolve,
|
|
14
|
+
* reject, or already-aborted) — a leaked listener on a long-lived signal
|
|
15
|
+
* accumulates across a turn's many retries.
|
|
16
|
+
*/
|
|
17
|
+
export function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
18
|
+
return new Promise<void>((resolve, reject) => {
|
|
19
|
+
if (signal?.aborted) {
|
|
20
|
+
reject(abortReason(signal));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let onAbort: (() => void) | undefined;
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
if (signal && onAbort) signal.removeEventListener('abort', onAbort);
|
|
26
|
+
resolve();
|
|
27
|
+
}, Math.max(0, ms));
|
|
28
|
+
if (signal) {
|
|
29
|
+
onAbort = (): void => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
reject(abortReason(signal));
|
|
32
|
+
};
|
|
33
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
39
|
+
// Prefer the caller-supplied reason; fall back to a standard AbortError.
|
|
40
|
+
const reason = (signal as { reason?: unknown }).reason;
|
|
41
|
+
if (reason !== undefined) return reason;
|
|
42
|
+
return new DOMException('The operation was aborted', 'AbortError');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Exponential back-off for a 1-based retry `attempt`: `baseMs * 2^(attempt-1)`,
|
|
47
|
+
* clamped to `[baseMs, maxMs]` (default cap 30_000ms). `attempt <= 1` yields
|
|
48
|
+
* `baseMs`. Matches the schedule both modes used before extraction.
|
|
49
|
+
*/
|
|
50
|
+
export function nextBackoffMs(attempt: number, baseMs: number, maxMs = 30_000): number {
|
|
51
|
+
const exp = Math.max(0, Math.floor(attempt) - 1);
|
|
52
|
+
return Math.min(maxMs, baseMs * 2 ** exp);
|
|
53
|
+
}
|
|
@@ -35,6 +35,28 @@ function safeStringifyOutput(output: unknown): string {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* An image ContentBlock extracted from an image-shaped tool output. A tool can
|
|
40
|
+
* return either `{ mediaType, base64 }` (the desktop screenshot/clipboard shape)
|
|
41
|
+
* or the provider-native `{ type:'image', mediaType, data }` — both normalize to
|
|
42
|
+
* the same provider `image` block so the model SEES the pixels instead of a
|
|
43
|
+
* stringified blob of base64.
|
|
44
|
+
*/
|
|
45
|
+
function imageBlockFromOutput(output: unknown): Extract<ContentBlock, { type: 'image' }> | null {
|
|
46
|
+
if (typeof output !== 'object' || output === null) return null;
|
|
47
|
+
const o = output as { type?: unknown; mediaType?: unknown; base64?: unknown; data?: unknown };
|
|
48
|
+
if (typeof o.mediaType !== 'string') return null;
|
|
49
|
+
// `{ mediaType, base64 }` (raw shape) or `{ type:'image', mediaType, data }`.
|
|
50
|
+
const data =
|
|
51
|
+
typeof o.base64 === 'string'
|
|
52
|
+
? o.base64
|
|
53
|
+
: o.type === 'image' && typeof o.data === 'string'
|
|
54
|
+
? o.data
|
|
55
|
+
: null;
|
|
56
|
+
if (data === null) return null;
|
|
57
|
+
return { type: 'image', mediaType: o.mediaType, data };
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
/** Appended to the system prompt while elision is active (see projection). */
|
|
39
61
|
export const ELISION_SYSTEM_NOTE =
|
|
40
62
|
'Context note: to stay within budget, older turns may appear as stubs like ' +
|
|
@@ -461,6 +483,10 @@ export function projectMessages(
|
|
|
461
483
|
// Stub bulky old tool output to a recall-able marker (decision shared
|
|
462
484
|
// with estimateContextTokens via toolResultStubbed).
|
|
463
485
|
let text: string;
|
|
486
|
+
// An image-shaped output (a screenshot/clipboard grab) is carried as a
|
|
487
|
+
// provider `image` block so the model SEES the pixels — only when the
|
|
488
|
+
// result isn't stubbed (elided) or an error and the call succeeded.
|
|
489
|
+
let image: Extract<ContentBlock, { type: 'image' }> | null = null;
|
|
464
490
|
if (toolResultStubbed(e, el)) {
|
|
465
491
|
const recalled = el.recalledCallIds.has(e.callId) || el.recalledSeqs.has(e.seq);
|
|
466
492
|
text = toolResultStub(e.callId, toolResultBytes(e.output), recalled);
|
|
@@ -470,13 +496,18 @@ export function projectMessages(
|
|
|
470
496
|
// Rich result (e.g. a file diff): the model only needs the short
|
|
471
497
|
// `forModel` summary — the structured `display` is for channels.
|
|
472
498
|
text = e.output.forModel;
|
|
499
|
+
} else if (e.ok && (image = imageBlockFromOutput(e.output))) {
|
|
500
|
+
// Short marker satisfies the tool_use→tool_result pairing; the image
|
|
501
|
+
// block (appended below) carries the actual pixels.
|
|
502
|
+
text = '[image returned by tool — see attached image]';
|
|
473
503
|
} else {
|
|
474
504
|
text = safeStringifyOutput(e.output);
|
|
475
505
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
506
|
+
const content: ContentBlock[] = [
|
|
507
|
+
{ type: 'tool_result', toolUseId: e.callId, content: text, isError: !e.ok },
|
|
508
|
+
];
|
|
509
|
+
if (image) content.push(image);
|
|
510
|
+
messages.push({ role: 'tool_result', content });
|
|
480
511
|
recordStable(e.seq);
|
|
481
512
|
break;
|
|
482
513
|
}
|
package/src/mode-helpers.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
type StreamResult,
|
|
26
26
|
} from './mode/collect-stream.js';
|
|
27
27
|
export { runSingleShotTurn } from './mode/single-shot.js';
|
|
28
|
+
export { sleepWithAbort, nextBackoffMs } from './mode/abort-backoff.js';
|
|
28
29
|
export {
|
|
29
30
|
createStuckLoopDetector,
|
|
30
31
|
type StuckLoopDetector,
|
package/src/package-root.test.ts
CHANGED
|
@@ -44,8 +44,6 @@ describe('@moxxy/sdk package root', () => {
|
|
|
44
44
|
// re-export on the main barrel is caught (see .dependency-cruiser.cjs
|
|
45
45
|
// `no-node-builtins-in-renderer`).
|
|
46
46
|
const NODE_ONLY_VALUE_EXPORTS = [
|
|
47
|
-
'spawnCliTunnel',
|
|
48
|
-
'isCliTunnelAvailable',
|
|
49
47
|
'writeFileAtomic',
|
|
50
48
|
'writeFileAtomicSync',
|
|
51
49
|
'moxxyHome',
|
package/src/plugin.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface PluginSpec {
|
|
|
41
41
|
readonly viewRenderers?: ReadonlyArray<ViewRendererDef>;
|
|
42
42
|
/**
|
|
43
43
|
* Tunnel providers that expose the local web surface publicly (e.g.
|
|
44
|
-
*
|
|
44
|
+
* the proxy relay). One active per session; core seeds a `localhost` no-op.
|
|
45
45
|
*/
|
|
46
46
|
readonly tunnelProviders?: ReadonlyArray<TunnelProviderDef>;
|
|
47
47
|
readonly channels?: ReadonlyArray<ChannelDef>;
|