@method-ai-hq/sdk 0.18.0 → 0.18.2
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 +1 -1
- package/dist/packages/sdk/package.json +1 -1
- package/dist/packages/sdk/src/current-runtime.js +11 -5
- package/dist/packages/sdk/src/local-cli.js +4 -4
- package/dist/packages/sdk/src/method-client.d.ts +2 -0
- package/dist/packages/sdk/src/method-client.js +6 -2
- package/dist/packages/sdk/src/method.js +16 -3
- package/dist/packages/sdk/src/outbox.d.ts +1 -1
- package/dist/packages/sdk/src/outbox.js +6 -5
- package/dist/packages/sdk/src/run-saved.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
MIT-licensed JavaScript API and the `method` CLI. Source and releases: https://github.com/method-ai-hq/method-sdk
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.18.
|
|
6
|
+
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.18.2/method-ai-hq-sdk-0.18.2.tgz
|
|
7
7
|
npx method --version
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -60,9 +60,6 @@ export async function runCurrentFile(file, flags, syncFactory, onEvent, client =
|
|
|
60
60
|
const json = (path) => path ? JSON.parse(readFileSync(authoringPath(path), "utf8")) : undefined;
|
|
61
61
|
if (flags.resume)
|
|
62
62
|
assertCheckpointExecutor(json(join(flags['run-dir'], 'checkpoint.json')));
|
|
63
|
-
const setup = await localSetup(file, flags, baseConfig);
|
|
64
|
-
let config = setup.config;
|
|
65
|
-
const sourceRoot = setup.sourceRoot;
|
|
66
63
|
const method = (await import('./authoring.js')).readDocument(file);
|
|
67
64
|
const controller = new AbortController();
|
|
68
65
|
const stop = () => controller.abort(Object.assign(new Error("Interrupted by operator"), { code: "interrupted" }));
|
|
@@ -79,8 +76,17 @@ export async function runCurrentFile(file, flags, syncFactory, onEvent, client =
|
|
|
79
76
|
catch (error) {
|
|
80
77
|
process.stderr.write(`Run records stay on this computer: ${error.message}\n`);
|
|
81
78
|
}
|
|
82
|
-
// Records the start; sends nothing. The outbox starts when the run starts.
|
|
83
|
-
|
|
79
|
+
// Records the start; sends nothing. The outbox starts when the run starts. An inputs file that cannot be read fails
|
|
80
|
+
// the run below, with this record and no inputs.
|
|
81
|
+
let inputs = {};
|
|
82
|
+
try {
|
|
83
|
+
inputs = json(flags.inputs) ?? {};
|
|
84
|
+
}
|
|
85
|
+
catch { /* the run's setup failure */ }
|
|
86
|
+
await sync?.start(method, inputs, {}, false);
|
|
87
|
+
const setup = await localSetup(file, flags, baseConfig);
|
|
88
|
+
let config = setup.config;
|
|
89
|
+
const sourceRoot = setup.sourceRoot;
|
|
84
90
|
// A run with its own OpenRouter key (config.classification.api_key_env) needs no Method sign-in for classification.
|
|
85
91
|
const needs = accountNeeds(method, config);
|
|
86
92
|
const classification = needs.classification ? managedClassification(client) : undefined;
|
|
@@ -85,12 +85,12 @@ async function accountSync(path, flags) {
|
|
|
85
85
|
return { client, outbox, version, sync: () => new Sync(client, runDir, state.workflow_id, state.version_id, state.id, readDocument(path).run_data === "device", outbox) };
|
|
86
86
|
}
|
|
87
87
|
const method = readDocument(path);
|
|
88
|
-
|
|
89
|
-
const needs = accountNeeds(method, setup.config);
|
|
90
|
-
if (needs
|
|
88
|
+
// A setup that fails here fails the run too, and the run sends its failed record.
|
|
89
|
+
const needs = await localSetup(path, flags).then(setup => accountNeeds(method, setup.config), () => undefined);
|
|
90
|
+
if (needs?.classification && !client.token())
|
|
91
91
|
await client.login();
|
|
92
92
|
if (!client.token()) {
|
|
93
|
-
process.stderr.write(`Sign in to keep versions and runs in your account${needs
|
|
93
|
+
process.stderr.write(`Sign in to keep versions and runs in your account${needs?.models ? " and to use hosted models" : ""}: method login\n`);
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
96
|
const version = await prepareVersion(client, outbox, path);
|
|
@@ -22,6 +22,8 @@ export declare class MethodClient {
|
|
|
22
22
|
onProgress?: (sent: number) => void;
|
|
23
23
|
attempts?: number;
|
|
24
24
|
}): Promise<Uint8Array>;
|
|
25
|
+
/** A lost connection: code network, and the message says which server. */
|
|
26
|
+
private unreachable;
|
|
25
27
|
/** Send bytes in chunks and read the answer in chunks; a watchdog aborts when no bytes moved for stallMs. */
|
|
26
28
|
private send;
|
|
27
29
|
login(openBrowser?: (url: string) => void): Promise<void>;
|
|
@@ -75,7 +75,7 @@ export class MethodClient {
|
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
if (lost)
|
|
78
|
-
throw
|
|
78
|
+
throw this.unreachable(method, path, error);
|
|
79
79
|
throw error;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -109,11 +109,15 @@ export class MethodClient {
|
|
|
109
109
|
}
|
|
110
110
|
catch (error) {
|
|
111
111
|
if (attempt >= (options.attempts ?? 3) || error.retryable === false)
|
|
112
|
-
throw error;
|
|
112
|
+
throw error?.name === 'TypeError' ? this.unreachable(data ? 'PUT' : 'GET', path, error) : error;
|
|
113
113
|
await new Promise(resolve => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
/** A lost connection: code network, and the message says which server. */
|
|
118
|
+
unreachable(method, path, error) {
|
|
119
|
+
return Object.assign(Error(`Method cannot be reached at ${this.server} (${method} ${path}): ${error.cause?.code ?? error.message}. Check the connection and run again.`), { code: "network" });
|
|
120
|
+
}
|
|
117
121
|
/** Send bytes in chunks and read the answer in chunks; a watchdog aborts when no bytes moved for stallMs. */
|
|
118
122
|
async send(path, method, options) {
|
|
119
123
|
const stallMs = options.stallMs ?? 15_000, controller = new AbortController();
|
|
@@ -4,7 +4,7 @@ import runtimeInfo from '@withmethod/runtime/package.json' with { type: 'json' }
|
|
|
4
4
|
import { startRunWorker, waitForRun, cancelRun, workerAlive } from './run-worker.js';
|
|
5
5
|
import { methodCache } from './prepare.js';
|
|
6
6
|
import { restorePackage } from './method-files.js';
|
|
7
|
-
import { publish } from './versions.js';
|
|
7
|
+
import { publish, methodContent } from './versions.js';
|
|
8
8
|
import { runSaved } from './run-saved.js';
|
|
9
9
|
import { bindInput, bindConnection } from './bindings.js';
|
|
10
10
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -24,6 +24,18 @@ import { methodHelp, overviewHelp as help } from "./method-help.js";
|
|
|
24
24
|
function safePath(path) {
|
|
25
25
|
return authoringPath(path);
|
|
26
26
|
}
|
|
27
|
+
/** A run target that can save a version: a saved Method (ID or link), or a Method file that loads. */
|
|
28
|
+
function loads(target) {
|
|
29
|
+
if (!target || !existsSync(target))
|
|
30
|
+
return true;
|
|
31
|
+
try {
|
|
32
|
+
loadWorkflow(methodContent(readDocument(target)));
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
async function dispatchRunWorker(args, flags, extra = [], savedClient) {
|
|
28
40
|
if (process.env.METHOD_RUN_WORKER === '1' || (!/[/\\]method\.(?:js|ts)$/.test(process.argv[1] ?? '') && !flags.background))
|
|
29
41
|
return false;
|
|
@@ -44,8 +56,9 @@ async function dispatchRunWorker(args, flags, extra = [], savedClient) {
|
|
|
44
56
|
const resolved = await withInputsFrom(flags, directory, /\.method$/i.test(args[1] ?? '') ? args[1] : undefined, client);
|
|
45
57
|
workerArgs = [...withoutOptions(workerArgs, ['inputs-from', 'inputs']), '--inputs', resolved.inputs];
|
|
46
58
|
}
|
|
47
|
-
// A new run of a signed-in computer is synced; a resumed run keeps the link that it has.
|
|
48
|
-
|
|
59
|
+
// A new run of a signed-in computer is synced; a resumed run keeps the link that it has. A Method file that does not
|
|
60
|
+
// load saves no version, so its run sends no record and gets no link.
|
|
61
|
+
const server = !flags.resume && client?.token() && loads(args[1]) ? client.server : undefined;
|
|
49
62
|
await startRunWorker(workerArgs, directory, flags.background, server);
|
|
50
63
|
return true;
|
|
51
64
|
}
|
|
@@ -56,7 +56,7 @@ export declare function lastSaved(server: string, methodId: string): {
|
|
|
56
56
|
export declare function recordKnownVersion(server: string, methodId: string, versionId: string, versionNumber: number): void;
|
|
57
57
|
/** A checkout of a saved version: the next save of this Method on this computer has it as its parent. */
|
|
58
58
|
export declare function recordCheckout(server: string, methodId: string, versionId: string, versionNumber: number, workflow: unknown): void;
|
|
59
|
-
/** Errors that a retry can fix: no
|
|
59
|
+
/** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
|
|
60
60
|
export declare function retryable(error: any): boolean;
|
|
61
61
|
export declare class Outbox {
|
|
62
62
|
readonly client: MethodClient;
|
|
@@ -40,12 +40,13 @@ function recordSaved(item, versionNumber) {
|
|
|
40
40
|
writePrivateJson(savedFile(item.server), { ...savedVersions(item.server), [item.version_id]: { method_id: item.method_id, version_number: versionNumber } });
|
|
41
41
|
writePrivateJson(methodFile(item.server, item.method_id), { version_id: item.version_id, version_number: versionNumber, workflow: item.body.workflow });
|
|
42
42
|
}
|
|
43
|
-
/** Errors that a retry can fix: no
|
|
43
|
+
/** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
|
|
44
44
|
export function retryable(error) {
|
|
45
45
|
const status = error?.status ?? Number(/^(\d{3}):/.exec(String(error?.message))?.[1]);
|
|
46
|
-
return
|
|
46
|
+
return status ? status >= 500 || status === 429 || status === 408 : unreachable(error) || error?.code === 'stalled';
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/** The server cannot be reached. An error on this computer (an unreadable run folder) is not one. */
|
|
49
|
+
const unreachable = (error) => error?.code === 'network';
|
|
49
50
|
export class Outbox {
|
|
50
51
|
client;
|
|
51
52
|
options;
|
|
@@ -221,7 +222,7 @@ export class Outbox {
|
|
|
221
222
|
}
|
|
222
223
|
// No response, a server error, or too many requests: after 3 in a row, stop and keep the items for a later
|
|
223
224
|
// command. A finished run never waits for a server that keeps failing.
|
|
224
|
-
if (
|
|
225
|
+
if (unreachable(error) || status >= 500 || status === 429)
|
|
225
226
|
this.networkFailures++;
|
|
226
227
|
const attempt = (this.retryAt.get(key)?.attempt ?? 0) + 1;
|
|
227
228
|
this.retryAt.set(key, { at: Date.now() + Math.min(30_000, 500 * 2 ** (attempt - 1)), attempt });
|
|
@@ -256,7 +257,7 @@ export class Outbox {
|
|
|
256
257
|
}
|
|
257
258
|
catch (error) {
|
|
258
259
|
const status = error?.status ?? Number(/^(\d{3}):/.exec(String(error?.message))?.[1]);
|
|
259
|
-
if (
|
|
260
|
+
if (unreachable(error) || status >= 500 || status === 429)
|
|
260
261
|
this.networkFailures++;
|
|
261
262
|
}
|
|
262
263
|
}
|
|
@@ -48,10 +48,17 @@ export async function runSaved(saved, flags, client) {
|
|
|
48
48
|
return completed;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
flags = await withInputsFrom(flags, directory, undefined, client);
|
|
52
51
|
const inputFile = join(directory, 'requested-inputs.json');
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// The start line already linked to this run: inputs that cannot be read fail it below, after its start is recorded.
|
|
53
|
+
let inputs = {}, unreadable;
|
|
54
|
+
try {
|
|
55
|
+
flags = await withInputsFrom(flags, directory, undefined, client);
|
|
56
|
+
inputs = flags.inputs ? JSON.parse(readFileSync(resolve(flags.inputs), 'utf8')) : existsSync(inputFile) ? JSON.parse(readFileSync(inputFile, 'utf8')) : {};
|
|
57
|
+
writePrivateJson(inputFile, inputs);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
unreadable = error;
|
|
61
|
+
}
|
|
55
62
|
const setupFile = join(directory, 'setup.json');
|
|
56
63
|
const setupEvents = existsSync(setupFile) ? JSON.parse(readFileSync(setupFile, 'utf8')) : [];
|
|
57
64
|
setupEvents.push({ at: new Date().toISOString(), type: 'setup_started' });
|
|
@@ -61,6 +68,8 @@ export async function runSaved(saved, flags, client) {
|
|
|
61
68
|
let acquired = false, started = false, revision = 0;
|
|
62
69
|
const statePath = `/api/cli/methods/${encodeURIComponent(saved.workflow_id)}/state`;
|
|
63
70
|
try {
|
|
71
|
+
if (unreadable)
|
|
72
|
+
throw unreadable;
|
|
64
73
|
const resuming = !!flags.resume && existsSync(join(directory, 'checkpoint.json'));
|
|
65
74
|
if (resuming)
|
|
66
75
|
assertCheckpointExecutor(JSON.parse(readFileSync(join(directory, 'checkpoint.json'), 'utf8')));
|