@phnx-labs/agents-cli 1.20.34 → 1.20.36
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/CHANGELOG.md +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as os from 'os';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { getCloudDir } from '../state.js';
|
|
13
|
-
import { resolveDispatchRepos } from './types.js';
|
|
13
|
+
import { resolveDispatchRepos, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
14
14
|
import { parseSSE } from './stream.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
16
16
|
import { getAccountInfo } from '../agents.js';
|
|
@@ -286,6 +286,12 @@ export function buildDispatchBody(input) {
|
|
|
286
286
|
if (input.accountTokens && input.accountTokens.length > 0) {
|
|
287
287
|
body.account_tokens = input.accountTokens;
|
|
288
288
|
}
|
|
289
|
+
if (input.skills && input.skills.length > 0) {
|
|
290
|
+
body.skills = input.skills;
|
|
291
|
+
}
|
|
292
|
+
if (input.images && input.images.length > 0) {
|
|
293
|
+
body.images = input.images.slice(0, MAX_IMAGES_PER_DISPATCH);
|
|
294
|
+
}
|
|
289
295
|
return body;
|
|
290
296
|
}
|
|
291
297
|
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
@@ -332,8 +338,8 @@ export class RushCloudProvider {
|
|
|
332
338
|
cancel: true,
|
|
333
339
|
message: true,
|
|
334
340
|
multiRepo: true,
|
|
335
|
-
skills:
|
|
336
|
-
images:
|
|
341
|
+
skills: true,
|
|
342
|
+
images: true,
|
|
337
343
|
};
|
|
338
344
|
}
|
|
339
345
|
async dispatch(options) {
|
|
@@ -387,6 +393,8 @@ export class RushCloudProvider {
|
|
|
387
393
|
resolvedRepos,
|
|
388
394
|
accountManifest,
|
|
389
395
|
strategy,
|
|
396
|
+
skills: options.skills,
|
|
397
|
+
images: options.images,
|
|
390
398
|
});
|
|
391
399
|
let res = await api('POST', '/api/v1/cloud-runs', token, body);
|
|
392
400
|
// Server detects drift (new account or rotated token) by comparing the
|
|
@@ -429,6 +437,8 @@ export class RushCloudProvider {
|
|
|
429
437
|
resolvedRepos,
|
|
430
438
|
accountManifest,
|
|
431
439
|
accountTokens,
|
|
440
|
+
skills: options.skills,
|
|
441
|
+
images: options.images,
|
|
432
442
|
});
|
|
433
443
|
res = await api('POST', '/api/v1/cloud-runs', token, retryBody);
|
|
434
444
|
// Persist consent on first successful upload so we don't re-prompt
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Factory) implement, plus the shared task and event types that flow through
|
|
6
6
|
* the dispatch pipeline.
|
|
7
7
|
*/
|
|
8
|
+
import type { JobTrigger } from '../routines.js';
|
|
8
9
|
/**
|
|
9
10
|
* Identifier for a supported cloud dispatch backend.
|
|
10
11
|
*
|
|
@@ -119,6 +120,14 @@ export declare const MAX_IMAGES_PER_DISPATCH = 5;
|
|
|
119
120
|
export interface DispatchOptions {
|
|
120
121
|
prompt: string;
|
|
121
122
|
agent?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Event/webhook trigger to register with this dispatch. Set by
|
|
125
|
+
* `agents cloud run --on <event>`: instead of dispatching immediately, the
|
|
126
|
+
* dispatch is persisted as a trigger-bound routine so the local webhook
|
|
127
|
+
* receiver can fire it when the event arrives. Remote firing of the trigger
|
|
128
|
+
* is a follow-up; today the flag parses, validates, and persists.
|
|
129
|
+
*/
|
|
130
|
+
trigger?: JobTrigger;
|
|
122
131
|
/**
|
|
123
132
|
* Legacy single-repo target. Still honored: if `repos` is empty, this
|
|
124
133
|
* becomes the only repo. Providers that support multi-repo treat
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComputerClient } from '../computer-rpc.js';
|
|
2
|
+
import { type AppInfo } from '../../commands/computer-actions.js';
|
|
3
|
+
import type { VerbDispatcher } from './loop.js';
|
|
4
|
+
declare function rpcMethodFor(verb: string): string;
|
|
5
|
+
declare function toRpcParams(verb: string, input: Record<string, unknown>, pid?: number): Record<string, unknown>;
|
|
6
|
+
export declare function makeVerbDispatcher(client: ComputerClient): VerbDispatcher;
|
|
7
|
+
export { rpcMethodFor, toRpcParams };
|
|
8
|
+
export type { AppInfo };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Verb dispatcher for the `computer run` loop.
|
|
2
|
+
//
|
|
3
|
+
// Maps a loop VerbCall (CLI-verb name + friendly input) onto the EXISTING
|
|
4
|
+
// computer-helper RPC methods over the daemon socket. It reimplements no verb —
|
|
5
|
+
// it names the same RPC methods the explicit `agents computer <verb>` commands
|
|
6
|
+
// call, so the external-agent verb surface is untouched. The daemon stays the
|
|
7
|
+
// single authority on permissions and targeting.
|
|
8
|
+
import { resolveTargetPidDecision } from '../../commands/computer-actions.js';
|
|
9
|
+
// CLI verb -> daemon RPC method. Only the rename cases need listing; the rest
|
|
10
|
+
// pass through by name.
|
|
11
|
+
const RPC_METHOD = {
|
|
12
|
+
apps: 'list_apps',
|
|
13
|
+
'get-text': 'get_text',
|
|
14
|
+
'type-text': 'type_text',
|
|
15
|
+
'ax-action': 'ax_action',
|
|
16
|
+
'right-click': 'right_click',
|
|
17
|
+
launch: 'launch_app',
|
|
18
|
+
focus: 'set_focus',
|
|
19
|
+
raise: 'focus_window',
|
|
20
|
+
};
|
|
21
|
+
// Verbs that do not need a resolved target pid.
|
|
22
|
+
const NO_TARGET = new Set(['apps', 'launch']);
|
|
23
|
+
function rpcMethodFor(verb) {
|
|
24
|
+
return RPC_METHOD[verb] ?? verb;
|
|
25
|
+
}
|
|
26
|
+
// Translate the model's friendly input keys into the daemon's param names.
|
|
27
|
+
// pid is injected by the caller after target resolution.
|
|
28
|
+
function toRpcParams(verb, input, pid) {
|
|
29
|
+
const p = {};
|
|
30
|
+
if (pid != null)
|
|
31
|
+
p.pid = pid;
|
|
32
|
+
const copyIf = (from, to) => {
|
|
33
|
+
if (input[from] != null)
|
|
34
|
+
p[to] = input[from];
|
|
35
|
+
};
|
|
36
|
+
switch (verb) {
|
|
37
|
+
case 'describe':
|
|
38
|
+
copyIf('depth', 'max_depth');
|
|
39
|
+
break;
|
|
40
|
+
case 'screenshot':
|
|
41
|
+
copyIf('quality', 'quality');
|
|
42
|
+
break;
|
|
43
|
+
case 'get-text':
|
|
44
|
+
copyIf('id', 'element_id');
|
|
45
|
+
copyIf('maxChars', 'max_chars');
|
|
46
|
+
break;
|
|
47
|
+
case 'click':
|
|
48
|
+
case 'right-click':
|
|
49
|
+
case 'scroll':
|
|
50
|
+
copyIf('id', 'element_id');
|
|
51
|
+
copyIf('x', 'x');
|
|
52
|
+
copyIf('y', 'y');
|
|
53
|
+
copyIf('count', 'count');
|
|
54
|
+
copyIf('dx', 'dx');
|
|
55
|
+
copyIf('dy', 'dy');
|
|
56
|
+
break;
|
|
57
|
+
case 'type':
|
|
58
|
+
copyIf('id', 'element_id');
|
|
59
|
+
copyIf('x', 'x');
|
|
60
|
+
copyIf('y', 'y');
|
|
61
|
+
copyIf('text', 'text');
|
|
62
|
+
copyIf('commit', 'commit');
|
|
63
|
+
break;
|
|
64
|
+
case 'type-text':
|
|
65
|
+
copyIf('text', 'text');
|
|
66
|
+
copyIf('commit', 'commit');
|
|
67
|
+
break;
|
|
68
|
+
case 'key':
|
|
69
|
+
copyIf('keys', 'keys');
|
|
70
|
+
break;
|
|
71
|
+
case 'ax-action':
|
|
72
|
+
copyIf('id', 'element_id');
|
|
73
|
+
copyIf('action', 'action');
|
|
74
|
+
break;
|
|
75
|
+
case 'focus':
|
|
76
|
+
copyIf('id', 'element_id');
|
|
77
|
+
break;
|
|
78
|
+
case 'launch':
|
|
79
|
+
copyIf('bundle', 'bundle_id');
|
|
80
|
+
copyIf('path', 'path');
|
|
81
|
+
copyIf('name', 'name');
|
|
82
|
+
break;
|
|
83
|
+
case 'wait': {
|
|
84
|
+
if (input.duration != null)
|
|
85
|
+
p.duration_ms = input.duration;
|
|
86
|
+
copyIf('id', 'element_id');
|
|
87
|
+
copyIf('until', 'until');
|
|
88
|
+
const locator = {};
|
|
89
|
+
for (const k of ['role', 'label', 'identifier']) {
|
|
90
|
+
if (input[k] != null)
|
|
91
|
+
locator[k] = input[k];
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(locator).length > 0)
|
|
94
|
+
p.locator = locator;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
return p;
|
|
101
|
+
}
|
|
102
|
+
// Build a dispatcher bound to a live client. Every call is a single daemon
|
|
103
|
+
// round-trip; errors are returned (never thrown) so the loop can feed them
|
|
104
|
+
// back to the model as a tool_result.
|
|
105
|
+
export function makeVerbDispatcher(client) {
|
|
106
|
+
return async (call) => {
|
|
107
|
+
const verb = call.name;
|
|
108
|
+
const input = call.input ?? {};
|
|
109
|
+
let pid;
|
|
110
|
+
if (!NO_TARGET.has(verb) && !(verb === 'wait' && input.duration != null)) {
|
|
111
|
+
const resolved = await resolveTargetPidDecision(client, { pid: input.pid, bundle: input.bundle });
|
|
112
|
+
if (!resolved.ok)
|
|
113
|
+
return { ok: false, error: resolved.error };
|
|
114
|
+
pid = resolved.pid;
|
|
115
|
+
}
|
|
116
|
+
const method = rpcMethodFor(verb);
|
|
117
|
+
const params = toRpcParams(verb, input, pid);
|
|
118
|
+
const res = await client.call(method, params);
|
|
119
|
+
if (res.error)
|
|
120
|
+
return { ok: false, error: `${res.error.code}: ${res.error.message}` };
|
|
121
|
+
return { ok: true, result: res.result ?? {} };
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Exposed for tests/diagnostics.
|
|
125
|
+
export { rpcMethodFor, toRpcParams };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface VerbCall {
|
|
2
|
+
name: string;
|
|
3
|
+
input: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface VerbResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
result?: Record<string, unknown>;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LoopStep {
|
|
11
|
+
call: VerbCall;
|
|
12
|
+
result: VerbResult;
|
|
13
|
+
visionFallback?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface LoopState {
|
|
16
|
+
task: string;
|
|
17
|
+
steps: LoopStep[];
|
|
18
|
+
visionMode: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ModelDecision {
|
|
21
|
+
toolCalls: VerbCall[];
|
|
22
|
+
done?: {
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export type ModelResponder = (state: LoopState) => ModelDecision | Promise<ModelDecision>;
|
|
27
|
+
export type VerbDispatcher = (call: VerbCall) => VerbResult | Promise<VerbResult>;
|
|
28
|
+
export interface LoopConfig {
|
|
29
|
+
task: string;
|
|
30
|
+
responder: ModelResponder;
|
|
31
|
+
dispatch: VerbDispatcher;
|
|
32
|
+
maxSteps: number;
|
|
33
|
+
onEvent?: (event: LoopEvent) => void;
|
|
34
|
+
}
|
|
35
|
+
export type LoopEvent = {
|
|
36
|
+
kind: 'turn';
|
|
37
|
+
index: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'dispatch';
|
|
40
|
+
call: VerbCall;
|
|
41
|
+
result: VerbResult;
|
|
42
|
+
visionFallback?: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'vision_switch';
|
|
45
|
+
describe: VerbResult;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'done';
|
|
48
|
+
text: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'max_steps';
|
|
51
|
+
};
|
|
52
|
+
export interface LoopResult {
|
|
53
|
+
status: 'done' | 'max_steps';
|
|
54
|
+
finalText?: string;
|
|
55
|
+
steps: LoopStep[];
|
|
56
|
+
dispatched: string[];
|
|
57
|
+
visionMode: boolean;
|
|
58
|
+
turns: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const AX_OPAQUE_ELEMENT_THRESHOLD = 3;
|
|
61
|
+
export declare function isAxOpaque(result: VerbResult): boolean;
|
|
62
|
+
export declare function runComputerLoop(config: LoopConfig): Promise<LoopResult>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The embedded observe -> act -> verify loop behind `agents computer run`.
|
|
2
|
+
//
|
|
3
|
+
// This module owns ONLY the decision logic: given a reasoning model (a
|
|
4
|
+
// boundary, injected as `responder`) and a verb dispatcher (the existing
|
|
5
|
+
// computer-helper RPC surface, injected as `dispatch`), it drives the model's
|
|
6
|
+
// tool calls against the daemon, step by step, until the model declares the
|
|
7
|
+
// task done or `maxSteps` is hit.
|
|
8
|
+
//
|
|
9
|
+
// The model and the daemon are boundaries — neither is code under test. The
|
|
10
|
+
// loop itself (verb ordering, max-steps, the AX -> vision fallback) is the
|
|
11
|
+
// unit under test. See loop.test.ts, which injects a scripted fake responder
|
|
12
|
+
// and a fake dispatcher.
|
|
13
|
+
// Below this many AX elements the tree is treated as opaque — a WebView, a
|
|
14
|
+
// Qt/Electron surface that never populated its AX tree, or a canvas app. The
|
|
15
|
+
// model then works from pixels + coordinate clicks instead of element ids.
|
|
16
|
+
export const AX_OPAQUE_ELEMENT_THRESHOLD = 3;
|
|
17
|
+
// Describe-time heuristic. An explicit `ax_opaque: true` from the daemon wins.
|
|
18
|
+
// Otherwise: a failed describe is NOT opaque (it's an error, surface it), and a
|
|
19
|
+
// successful one is opaque when its element_count is at/under the threshold or
|
|
20
|
+
// its tree has no children. Pure so the rule is unit-testable in isolation.
|
|
21
|
+
export function isAxOpaque(result) {
|
|
22
|
+
const r = result.result ?? {};
|
|
23
|
+
if (r.ax_opaque === true)
|
|
24
|
+
return true;
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
return false;
|
|
27
|
+
const count = typeof r.element_count === 'number' ? r.element_count : undefined;
|
|
28
|
+
if (count !== undefined)
|
|
29
|
+
return count <= AX_OPAQUE_ELEMENT_THRESHOLD;
|
|
30
|
+
const tree = r.tree;
|
|
31
|
+
const children = tree && Array.isArray(tree.children) ? tree.children : [];
|
|
32
|
+
return children.length === 0;
|
|
33
|
+
}
|
|
34
|
+
// Drive the loop to completion. Returns the transcript, the ordered verb names
|
|
35
|
+
// dispatched, and whether the vision fallback ever fired.
|
|
36
|
+
export async function runComputerLoop(config) {
|
|
37
|
+
const state = { task: config.task, steps: [], visionMode: false };
|
|
38
|
+
const dispatched = [];
|
|
39
|
+
const emit = config.onEvent ?? (() => { });
|
|
40
|
+
let turns = 0;
|
|
41
|
+
while (turns < config.maxSteps) {
|
|
42
|
+
emit({ kind: 'turn', index: turns });
|
|
43
|
+
turns++;
|
|
44
|
+
const decision = await config.responder(state);
|
|
45
|
+
if (decision.done) {
|
|
46
|
+
emit({ kind: 'done', text: decision.done.text });
|
|
47
|
+
return {
|
|
48
|
+
status: 'done',
|
|
49
|
+
finalText: decision.done.text,
|
|
50
|
+
steps: state.steps,
|
|
51
|
+
dispatched,
|
|
52
|
+
visionMode: state.visionMode,
|
|
53
|
+
turns,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
for (const call of decision.toolCalls) {
|
|
57
|
+
const result = await config.dispatch(call);
|
|
58
|
+
dispatched.push(call.name);
|
|
59
|
+
// AX -> vision fallback. When a describe comes back opaque, the element
|
|
60
|
+
// ids the model would click are useless, so the loop flips visionMode and
|
|
61
|
+
// immediately hands the model pixels: it auto-dispatches a screenshot of
|
|
62
|
+
// the same target. The explicit verbs stay untouched for external callers
|
|
63
|
+
// — this switch lives entirely in the loop.
|
|
64
|
+
if (call.name === 'describe' && isAxOpaque(result)) {
|
|
65
|
+
state.visionMode = true;
|
|
66
|
+
state.steps.push({ call, result, visionFallback: true });
|
|
67
|
+
emit({ kind: 'dispatch', call, result, visionFallback: true });
|
|
68
|
+
emit({ kind: 'vision_switch', describe: result });
|
|
69
|
+
const shotCall = { name: 'screenshot', input: pickTargetInput(call.input) };
|
|
70
|
+
const shot = await config.dispatch(shotCall);
|
|
71
|
+
dispatched.push(shotCall.name);
|
|
72
|
+
state.steps.push({ call: shotCall, result: shot });
|
|
73
|
+
emit({ kind: 'dispatch', call: shotCall, result: shot });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
state.steps.push({ call, result });
|
|
77
|
+
emit({ kind: 'dispatch', call, result });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
emit({ kind: 'max_steps' });
|
|
81
|
+
return {
|
|
82
|
+
status: 'max_steps',
|
|
83
|
+
steps: state.steps,
|
|
84
|
+
dispatched,
|
|
85
|
+
visionMode: state.visionMode,
|
|
86
|
+
turns,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Carry the target selector (pid/bundle) from the describe call onto the
|
|
90
|
+
// auto-injected screenshot so the vision fallback captures the same surface.
|
|
91
|
+
function pickTargetInput(input) {
|
|
92
|
+
const out = {};
|
|
93
|
+
if (input.pid != null)
|
|
94
|
+
out.pid = input.pid;
|
|
95
|
+
if (input.bundle != null)
|
|
96
|
+
out.bundle = input.bundle;
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LoopState, ModelDecision, ModelResponder } from './loop.js';
|
|
2
|
+
interface ToolSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
input_schema: {
|
|
6
|
+
type: 'object';
|
|
7
|
+
properties: Record<string, unknown>;
|
|
8
|
+
required?: string[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare const COMPUTER_TOOLS: ToolSpec[];
|
|
12
|
+
export interface ClaudeResponderConfig {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
maxTokens?: number;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_CLAUDE_BASE_URL = "https://api.anthropic.com";
|
|
20
|
+
export declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
|
|
21
|
+
export declare const ANTHROPIC_VERSION = "2023-06-01";
|
|
22
|
+
export declare function resolveApiKey(config: ClaudeResponderConfig, env?: NodeJS.ProcessEnv): string;
|
|
23
|
+
export declare function buildSystemPrompt(state: LoopState): string;
|
|
24
|
+
type AnthropicBlock = {
|
|
25
|
+
type: 'text';
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'tool_use';
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
input: Record<string, unknown>;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'tool_result';
|
|
34
|
+
tool_use_id: string;
|
|
35
|
+
content: string;
|
|
36
|
+
};
|
|
37
|
+
interface AnthropicMessage {
|
|
38
|
+
role: 'user' | 'assistant';
|
|
39
|
+
content: AnthropicBlock[];
|
|
40
|
+
}
|
|
41
|
+
export declare function buildMessages(state: LoopState): AnthropicMessage[];
|
|
42
|
+
export declare function parseDecision(body: unknown): ModelDecision;
|
|
43
|
+
export declare function makeClaudeResponder(config?: ClaudeResponderConfig): ModelResponder;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Default reasoning model for the `computer run` loop.
|
|
2
|
+
//
|
|
3
|
+
// This is a BOUNDARY, not loop logic. It turns the loop's LoopState into an
|
|
4
|
+
// Anthropic Messages API request (tool use), sends it to the Claude API by
|
|
5
|
+
// default OR to a configurable base URL (Ollama / vLLM / LiteLLM serving the
|
|
6
|
+
// Anthropic wire shape) for local/offline parity, and parses the reply back
|
|
7
|
+
// into a ModelDecision the loop can act on.
|
|
8
|
+
//
|
|
9
|
+
// The loop (loop.ts) never imports this — it takes a ModelResponder callback,
|
|
10
|
+
// so the unit tests inject a scripted fake instead of ever hitting a network.
|
|
11
|
+
const TARGET_PROPS = {
|
|
12
|
+
bundle: { type: 'string', description: 'Bundle id of the target app (default: frontmost allow-listed app)' },
|
|
13
|
+
pid: { type: 'number', description: 'Target pid (overrides bundle)' },
|
|
14
|
+
};
|
|
15
|
+
export const COMPUTER_TOOLS = [
|
|
16
|
+
{ name: 'apps', description: 'List allow-listed apps currently running.', input_schema: { type: 'object', properties: {} } },
|
|
17
|
+
{
|
|
18
|
+
name: 'describe',
|
|
19
|
+
description: 'Dump the accessibility tree of the target app. Element ids feed click/type via id. If the tree is opaque the loop auto-captures a screenshot for you.',
|
|
20
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, depth: { type: 'number' } } },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'screenshot',
|
|
24
|
+
description: 'Capture the target app window as an image (the vision path — use coordinate clicks off it).',
|
|
25
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'get-text',
|
|
29
|
+
description: 'Extract visible text from the target app (or a subtree via id).',
|
|
30
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' } } },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'click',
|
|
34
|
+
description: 'Click an element (id from describe) or a screen coordinate (x,y). Use x,y after a vision screenshot.',
|
|
35
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' } } },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'type',
|
|
39
|
+
description: 'Set a field value (id) or paste at a coordinate. Set commit=true to submit.',
|
|
40
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, commit: { type: 'boolean' } }, required: ['text'] },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'type-text',
|
|
44
|
+
description: 'Type a unicode string into the focused field. Focus it first with click or focus.',
|
|
45
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, text: { type: 'string' }, commit: { type: 'boolean' } }, required: ['text'] },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'key',
|
|
49
|
+
description: 'Send a key chord, e.g. "cmd+s", "enter", "esc".',
|
|
50
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, keys: { type: 'string' } }, required: ['keys'] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'launch',
|
|
54
|
+
description: 'Launch an app by bundle id, path, or name.',
|
|
55
|
+
input_schema: { type: 'object', properties: { bundle: { type: 'string' }, path: { type: 'string' }, name: { type: 'string' } } },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'wait',
|
|
59
|
+
description: 'Wait for a duration (ms) or for an element to appear.',
|
|
60
|
+
input_schema: { type: 'object', properties: { ...TARGET_PROPS, duration: { type: 'number' }, id: { type: 'string' }, role: { type: 'string' }, label: { type: 'string' } } },
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
export const DEFAULT_CLAUDE_BASE_URL = 'https://api.anthropic.com';
|
|
64
|
+
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
65
|
+
export const ANTHROPIC_VERSION = '2023-06-01';
|
|
66
|
+
// Resolve the API key: explicit config, then the standard Anthropic env var,
|
|
67
|
+
// then a computer-run-specific override. A local Anthropic-shaped endpoint
|
|
68
|
+
// (Ollama/vLLM/LiteLLM) usually ignores the key, so an empty key is allowed
|
|
69
|
+
// when a non-default base URL is set.
|
|
70
|
+
export function resolveApiKey(config, env = process.env) {
|
|
71
|
+
return config.apiKey || env.AGENTS_COMPUTER_API_KEY || env.ANTHROPIC_API_KEY || '';
|
|
72
|
+
}
|
|
73
|
+
// Build the system prompt. In vision mode we steer the model onto coordinate
|
|
74
|
+
// clicks off screenshots, because element ids from an opaque tree are useless.
|
|
75
|
+
export function buildSystemPrompt(state) {
|
|
76
|
+
const lines = [
|
|
77
|
+
'You are driving a macOS desktop through accessibility + vision tools to accomplish a task.',
|
|
78
|
+
'Loop: observe (describe / screenshot / get-text), act (click / type / key), then verify the result before the next step.',
|
|
79
|
+
'Prefer element ids from describe. When you are confident the task is complete, stop calling tools and reply with a short completion summary.',
|
|
80
|
+
`Task: ${state.task}`,
|
|
81
|
+
];
|
|
82
|
+
if (state.visionMode) {
|
|
83
|
+
lines.push('NOTE: the accessibility tree for the current surface is opaque (WebView / canvas). Work from the screenshot and click by x,y coordinates instead of element ids.');
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
// Reconstruct the Anthropic message array from the loop transcript. We rebuild
|
|
88
|
+
// from scratch each turn (the loop holds canonical state), synthesizing stable
|
|
89
|
+
// tool_use ids per step so each tool_result pairs with its call.
|
|
90
|
+
export function buildMessages(state) {
|
|
91
|
+
const messages = [
|
|
92
|
+
{ role: 'user', content: [{ type: 'text', text: `Begin. Task: ${state.task}` }] },
|
|
93
|
+
];
|
|
94
|
+
state.steps.forEach((step, i) => {
|
|
95
|
+
const id = `call_${i}`;
|
|
96
|
+
messages.push({ role: 'assistant', content: [{ type: 'tool_use', id, name: step.call.name, input: step.call.input }] });
|
|
97
|
+
const summary = step.result.ok
|
|
98
|
+
? JSON.stringify(step.result.result ?? {}).slice(0, 4000)
|
|
99
|
+
: `error: ${step.result.error ?? 'unknown'}`;
|
|
100
|
+
const note = step.visionFallback ? ' [ax tree opaque — auto-captured screenshot follows; use coordinate clicks]' : '';
|
|
101
|
+
messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: summary + note }] });
|
|
102
|
+
});
|
|
103
|
+
return messages;
|
|
104
|
+
}
|
|
105
|
+
// Parse the Anthropic response body into a ModelDecision. tool_use blocks
|
|
106
|
+
// become tool calls; a reply with no tool calls is treated as the model
|
|
107
|
+
// finishing, with its text as the summary.
|
|
108
|
+
export function parseDecision(body) {
|
|
109
|
+
const content = body.content;
|
|
110
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
111
|
+
const toolCalls = [];
|
|
112
|
+
const texts = [];
|
|
113
|
+
for (const block of blocks) {
|
|
114
|
+
const b = block;
|
|
115
|
+
if (b.type === 'tool_use' && typeof b.name === 'string') {
|
|
116
|
+
toolCalls.push({ name: b.name, input: b.input ?? {} });
|
|
117
|
+
}
|
|
118
|
+
else if (b.type === 'text' && typeof b.text === 'string') {
|
|
119
|
+
texts.push(b.text);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (toolCalls.length === 0) {
|
|
123
|
+
return { toolCalls: [], done: { text: texts.join('\n').trim() || 'no further action' } };
|
|
124
|
+
}
|
|
125
|
+
return { toolCalls };
|
|
126
|
+
}
|
|
127
|
+
// The default responder: one Anthropic Messages call per turn.
|
|
128
|
+
export function makeClaudeResponder(config = {}) {
|
|
129
|
+
const baseUrl = (config.baseUrl || DEFAULT_CLAUDE_BASE_URL).replace(/\/+$/, '');
|
|
130
|
+
const model = config.model || DEFAULT_CLAUDE_MODEL;
|
|
131
|
+
const maxTokens = config.maxTokens ?? 1024;
|
|
132
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
133
|
+
const apiKey = resolveApiKey(config);
|
|
134
|
+
return async (state) => {
|
|
135
|
+
const res = await doFetch(`${baseUrl}/v1/messages`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'content-type': 'application/json',
|
|
139
|
+
'x-api-key': apiKey,
|
|
140
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
model,
|
|
144
|
+
max_tokens: maxTokens,
|
|
145
|
+
system: buildSystemPrompt(state),
|
|
146
|
+
tools: COMPUTER_TOOLS,
|
|
147
|
+
messages: buildMessages(state),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const text = await res.text().catch(() => '');
|
|
152
|
+
throw new Error(`model request failed: ${res.status} ${res.statusText} ${text.slice(0, 500)}`);
|
|
153
|
+
}
|
|
154
|
+
const body = await res.json();
|
|
155
|
+
return parseDecision(body);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, order-preserving async mapper.
|
|
3
|
+
*
|
|
4
|
+
* Runs `fn` over `items` with at most `concurrency` calls in flight, and — when
|
|
5
|
+
* `staggerMs` is set — spaces successive task starts at least that far apart so
|
|
6
|
+
* spawns trickle out instead of firing as one simultaneous burst. Results come
|
|
7
|
+
* back in input order regardless of completion order.
|
|
8
|
+
*
|
|
9
|
+
* The stagger matters beyond scheduling: a burst of identical child spawns (per-PID
|
|
10
|
+
* `lsof`, multi-dotfile scans) reads to behavioral EDR as recon/enumeration. A
|
|
11
|
+
* bounded, spread-out spawn rate produces the same data without the burst signature.
|
|
12
|
+
*/
|
|
13
|
+
export interface BoundedMapOptions {
|
|
14
|
+
/** Maximum number of `fn` calls running at once. Coerced to >= 1. */
|
|
15
|
+
concurrency: number;
|
|
16
|
+
/** Minimum spacing (ms) between successive task starts. 0 (default) = no spacing. */
|
|
17
|
+
staggerMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function mapBounded<T, R>(items: readonly T[], fn: (item: T, index: number) => Promise<R>, opts: BoundedMapOptions): Promise<R[]>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function delay(ms) {
|
|
2
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
export async function mapBounded(items, fn, opts) {
|
|
5
|
+
const concurrency = Math.max(1, Math.floor(opts.concurrency));
|
|
6
|
+
const stagger = Math.max(0, opts.staggerMs ?? 0);
|
|
7
|
+
const results = new Array(items.length);
|
|
8
|
+
if (items.length === 0)
|
|
9
|
+
return results;
|
|
10
|
+
let cursor = 0;
|
|
11
|
+
// Shared gate: the earliest time the next task is allowed to start. Updated
|
|
12
|
+
// atomically (no await between read and write) so parallel workers each claim
|
|
13
|
+
// a distinct slot spaced `stagger` apart.
|
|
14
|
+
let nextStart = 0;
|
|
15
|
+
async function worker() {
|
|
16
|
+
for (;;) {
|
|
17
|
+
const i = cursor++;
|
|
18
|
+
if (i >= items.length)
|
|
19
|
+
return;
|
|
20
|
+
if (stagger > 0) {
|
|
21
|
+
const now = performance.now();
|
|
22
|
+
const wait = Math.max(0, nextStart - now);
|
|
23
|
+
nextStart = Math.max(now, nextStart) + stagger;
|
|
24
|
+
if (wait > 0)
|
|
25
|
+
await delay(wait);
|
|
26
|
+
}
|
|
27
|
+
results[i] = await fn(items[i], i);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const workers = Math.min(concurrency, items.length);
|
|
31
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
32
|
+
return results;
|
|
33
|
+
}
|