@method-ai-hq/sdk 0.18.1 → 0.19.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 +1 -1
- package/dist/packages/sdk/package.json +6 -1
- package/dist/packages/sdk/src/current-runtime.d.ts +6 -1
- package/dist/packages/sdk/src/current-runtime.js +50 -5
- package/dist/packages/sdk/src/home.d.ts +30 -26
- package/dist/packages/sdk/src/home.js +164 -60
- package/dist/packages/sdk/src/improve.d.ts +25 -0
- package/dist/packages/sdk/src/improve.js +256 -10
- package/dist/packages/sdk/src/judge.d.ts +14 -2
- package/dist/packages/sdk/src/judge.js +73 -13
- package/dist/packages/sdk/src/local-cli.js +4 -6
- package/dist/packages/sdk/src/merge.d.ts +38 -0
- package/dist/packages/sdk/src/merge.js +65 -0
- package/dist/packages/sdk/src/method-client.js +1 -1
- package/dist/packages/sdk/src/method-help.js +11 -6
- package/dist/packages/sdk/src/method-sync.d.ts +7 -0
- package/dist/packages/sdk/src/method-sync.js +33 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +139 -14
- package/dist/packages/sdk/src/outbox.d.ts +28 -6
- package/dist/packages/sdk/src/outbox.js +13 -6
- package/dist/packages/sdk/src/proposals.d.ts +0 -28
- package/dist/packages/sdk/src/proposals.js +2 -129
- package/dist/packages/sdk/src/quality.d.ts +10 -1
- package/dist/packages/sdk/src/quality.js +34 -4
- package/dist/packages/sdk/src/run-saved.d.ts +4 -0
- package/dist/packages/sdk/src/run-saved.js +45 -5
- package/dist/packages/sdk/src/versions.js +5 -3
- package/package.json +6 -1
|
@@ -1,61 +1,11 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { loadWorkflow } from '../../workflow-language/src/validate.js';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
5
4
|
import { authoringPath, readDocument, writeDocument } from './authoring.js';
|
|
6
5
|
import { DEFAULT_SERVER } from './method-client.js';
|
|
7
6
|
import { writePrivateJson } from './files.js';
|
|
8
|
-
import {
|
|
7
|
+
import { methodIdPattern } from './versions.js';
|
|
9
8
|
import { localMethodFolder } from './local-judgments.js';
|
|
10
|
-
const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
-
/** Keys of ours in their order; a key that only theirs has goes after the key before it in theirs. */
|
|
12
|
-
function keyOrder(ours, theirs) {
|
|
13
|
-
const keys = Object.keys(ours), theirKeys = Object.keys(theirs);
|
|
14
|
-
theirKeys.forEach((key, index) => {
|
|
15
|
-
if (keys.includes(key))
|
|
16
|
-
return;
|
|
17
|
-
const before = theirKeys.slice(0, index).reverse().find(other => keys.includes(other));
|
|
18
|
-
keys.splice(before === undefined ? 0 : keys.indexOf(before) + 1, 0, key);
|
|
19
|
-
});
|
|
20
|
-
return keys;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Three-way merge of Method documents, per step and per top-level key. base is the version the proposal started from,
|
|
24
|
-
* ours is the local file, and theirs is the proposed version. A part that only one side changed takes that change; a part
|
|
25
|
-
* that both sides changed in different ways is a conflict.
|
|
26
|
-
*/
|
|
27
|
-
export function mergeMethod(base, ours, theirs) {
|
|
28
|
-
if (isDeepStrictEqual(ours, base))
|
|
29
|
-
return { clean: true, workflow: theirs };
|
|
30
|
-
if (isDeepStrictEqual(theirs, base) || isDeepStrictEqual(ours, theirs))
|
|
31
|
-
return { clean: true, workflow: ours };
|
|
32
|
-
const conflicts = [];
|
|
33
|
-
const pick = (path, b, o, t) => {
|
|
34
|
-
if (isDeepStrictEqual(o, t) || isDeepStrictEqual(b, t))
|
|
35
|
-
return o;
|
|
36
|
-
if (isDeepStrictEqual(b, o))
|
|
37
|
-
return t;
|
|
38
|
-
conflicts.push({ path, base: b, ours: o, theirs: t });
|
|
39
|
-
return o;
|
|
40
|
-
};
|
|
41
|
-
const merge = (path, b, o, t, nested) => {
|
|
42
|
-
const result = {};
|
|
43
|
-
for (const key of keyOrder(o, t)) {
|
|
44
|
-
const at = path ? `${path}.${key}` : key;
|
|
45
|
-
const value = nested(key) && isObject(o[key]) && isObject(t[key]) && isObject(b?.[key] ?? {})
|
|
46
|
-
? merge(at, b?.[key] ?? {}, o[key], t[key], () => false) : pick(at, b?.[key], o[key], t[key]);
|
|
47
|
-
if (value !== undefined)
|
|
48
|
-
result[key] = value;
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
};
|
|
52
|
-
const workflow = merge('', isObject(base) ? base : {}, ours, theirs, key => key === 'steps');
|
|
53
|
-
return conflicts.length ? { clean: false, conflicts } : { clean: true, workflow };
|
|
54
|
-
}
|
|
55
|
-
/** The steps whose definition differs between two documents. */
|
|
56
|
-
export function changedSteps(before, after) {
|
|
57
|
-
return [...new Set([...Object.keys(before?.steps ?? {}), ...Object.keys(after?.steps ?? {})])].filter(id => !isDeepStrictEqual(before?.steps?.[id], after?.steps?.[id]));
|
|
58
|
-
}
|
|
59
9
|
/** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
|
|
60
10
|
export function writeMethodFile(file, id, content) {
|
|
61
11
|
const { format, ...rest } = content;
|
|
@@ -91,83 +41,6 @@ export function localProposals(file) {
|
|
|
91
41
|
return { ...record, proposed_workflow: record.proposed_workflow ?? null, path };
|
|
92
42
|
}).filter(record => record.file === basename(file)).sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
|
|
93
43
|
}
|
|
94
|
-
async function accountProposal(client, id, options = {}) {
|
|
95
|
-
const { proposal } = await client.request(`/api/proposals/${encodeURIComponent(id)}`, 'GET', undefined, true, options);
|
|
96
|
-
return { ...proposal, source: 'account', proposed_workflow: proposal.proposed_workflow ?? null };
|
|
97
|
-
}
|
|
98
|
-
async function newestAccepted(client, methodId, options = {}) {
|
|
99
|
-
const { proposals } = await client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals?status=accepted`, 'GET', undefined, true, options);
|
|
100
|
-
const accepted = (proposals ?? []).filter(p => p?.status === 'accepted').sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
|
|
101
|
-
return accepted[0] ? accountProposal(client, accepted[0].id, options) : undefined;
|
|
102
|
-
}
|
|
103
|
-
/** Merge a change Method made in the account into the file. A clean merge writes the file and records the change as applied. Never publishes. */
|
|
104
|
-
async function applyProposal(client, file, proposal, options = {}) {
|
|
105
|
-
const document = readDocument(file), ours = methodContent(document);
|
|
106
|
-
const merged = mergeMethod(proposal.base_workflow, ours, proposal.proposed_workflow);
|
|
107
|
-
if (!merged.clean)
|
|
108
|
-
return { status: 'conflict', conflicts: merged.conflicts };
|
|
109
|
-
try {
|
|
110
|
-
loadWorkflow(merged.workflow);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
return { status: 'invalid', error: error.message };
|
|
114
|
-
}
|
|
115
|
-
if (!isDeepStrictEqual(merged.workflow, ours))
|
|
116
|
-
writeMethodFile(file, typeof document.id === 'string' ? document.id : undefined, merged.workflow);
|
|
117
|
-
return { status: 'applied', steps: changedSteps(ours, merged.workflow), ...await markApplied(client, file, proposal, options) };
|
|
118
|
-
}
|
|
119
|
-
async function markApplied(client, file, proposal, options = {}) {
|
|
120
|
-
const version_id = (await currentVersion(file).catch(() => undefined))?.version_id;
|
|
121
|
-
try {
|
|
122
|
-
await client.request(`/api/proposals/${encodeURIComponent(proposal.id)}/applied`, 'POST', version_id ? { version_id } : {}, true, options);
|
|
123
|
-
return { version_id };
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
return { version_id, warning: `The file is changed, but Method did not record the proposal as applied: ${error.message}` };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const serverFlag = (args) => { const at = args.findIndex(arg => arg === '--server' || arg.startsWith('--server=')); return at < 0 ? undefined : args[at].startsWith('--server=') ? args[at].slice(9) : args[at + 1]; };
|
|
130
|
-
const skipAutoApply = new Set(['changes', 'judge', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', '__model', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
|
|
131
|
-
/**
|
|
132
|
-
* Before a command that takes a .method FILE: when this computer is signed in and Method changed the Method in the
|
|
133
|
-
* account (keeping run data in the account, or a fix of a Method that had not run on a computer), merge the change into
|
|
134
|
-
* the file and print one line. A conflict leaves the file unchanged and writes both versions of each conflicting part
|
|
135
|
-
* to a file for the coding agent to merge; the command goes on. Offline, signed out, or on any error, it does nothing.
|
|
136
|
-
*/
|
|
137
|
-
export async function autoApply(args, clientFactory) {
|
|
138
|
-
if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
|
|
139
|
-
return;
|
|
140
|
-
const target = args.slice(1).find(arg => !arg.startsWith('-') && /\.method$/i.test(arg));
|
|
141
|
-
if (!target || !existsSync(target))
|
|
142
|
-
return;
|
|
143
|
-
try {
|
|
144
|
-
const file = authoringPath(target), id = readDocument(file).id;
|
|
145
|
-
if (typeof id !== 'string' || !methodIdPattern.test(id))
|
|
146
|
-
return;
|
|
147
|
-
const client = clientFactory(serverFlag(args) ?? DEFAULT_SERVER);
|
|
148
|
-
if (!client.token())
|
|
149
|
-
return;
|
|
150
|
-
const options = { signal: AbortSignal.timeout(3000), stallMs: 3000 };
|
|
151
|
-
const proposal = await newestAccepted(client, id, options);
|
|
152
|
-
if (!proposal?.proposed_workflow)
|
|
153
|
-
return;
|
|
154
|
-
const result = await applyProposal(client, file, proposal, options);
|
|
155
|
-
if (result.status === 'applied') {
|
|
156
|
-
if (result.steps.length || result.warning)
|
|
157
|
-
process.stderr.write(`Method changed ${target} (${proposal.id}): ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''}${result.warning ? ` ${result.warning}` : ''}\n`);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// The agent merges by hand from the saved versions; the change is recorded as applied, so it is not merged again.
|
|
161
|
-
localMethodFolder(file);
|
|
162
|
-
const path = join(dirname(file), '.method', `conflict-${proposal.id}.json`);
|
|
163
|
-
writePrivateJson(path, { proposal_id: proposal.id, cause: proposal.cause, ...(result.status === 'conflict' ? { conflicts: result.conflicts } : { error: result.error }), proposed_workflow: proposal.proposed_workflow });
|
|
164
|
-
await markApplied(client, file, proposal, options).catch(() => undefined);
|
|
165
|
-
process.stderr.write(result.status === 'conflict'
|
|
166
|
-
? `Method's change ${proposal.id} (${proposal.cause}) conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Both versions of each part are in ${path}: edit ${target} so each part keeps both changes.\n`
|
|
167
|
-
: `Method's change ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. The changed version is in ${path}.\n`);
|
|
168
|
-
}
|
|
169
|
-
catch { /* offline or signed out: the command goes on without the change. */ }
|
|
170
|
-
}
|
|
171
44
|
/** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
|
|
172
45
|
export async function localRuns(file) {
|
|
173
46
|
const { recentRuns } = await import('./current-runtime.js');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MethodClient } from "./method-client.js";
|
|
1
2
|
/** The same runtime preparation as method run, without starting a run. Hosted models are used when signed in. */
|
|
2
3
|
export declare function preparedConfig(file: string, flags: {
|
|
3
4
|
workspace?: string;
|
|
@@ -16,8 +17,16 @@ export declare const defaultRunRoots: () => string[];
|
|
|
16
17
|
export declare function observeCommand(args: string[]): Promise<void>;
|
|
17
18
|
/** A fingerprint of a Method and the files it runs, to tell whether its cases were checked since the last change. */
|
|
18
19
|
export declare function methodFingerprint(file: string): string;
|
|
19
|
-
/**
|
|
20
|
+
/** A case as a person reads it: "Gráinne's case dates-conflict: the note" when the case names who made it. */
|
|
21
|
+
export declare const caseName: (c: {
|
|
22
|
+
id: string;
|
|
23
|
+
author?: string | null;
|
|
24
|
+
note?: unknown;
|
|
25
|
+
}) => string;
|
|
26
|
+
/** One line for method run when the Method changed since its cases last passed. Cases with an author are named. */
|
|
20
27
|
export declare function casesNotice(file: string): Promise<string | undefined>;
|
|
28
|
+
/** The name of the signed-in person, for the author of a new case; null when signed out or offline. Asked once per sign-in in a process. */
|
|
29
|
+
export declare function signedInName(client?: Pick<MethodClient, "server" | "request" | "token">): Promise<string | null>;
|
|
21
30
|
/** Run the cases of a Method with the same preparation as method run. */
|
|
22
31
|
export declare function runCases(file: string, flags: {
|
|
23
32
|
workspace?: string;
|
|
@@ -125,7 +125,9 @@ function recordChecked(file, report) {
|
|
|
125
125
|
mkdirSync(dirname(checkedFile(file)), { recursive: true, mode: 0o700 });
|
|
126
126
|
writePrivateJson(checkedFile(file), { file, fingerprint: methodFingerprint(file), passed: report.passed, cases: report.cases.length, at: new Date().toISOString() });
|
|
127
127
|
}
|
|
128
|
-
/**
|
|
128
|
+
/** A case as a person reads it: "Gráinne's case dates-conflict: the note" when the case names who made it. */
|
|
129
|
+
export const caseName = (c) => `${c.author}'s case ${c.id}: ${String(c.note ?? "").slice(0, 200).replace(/[.\s]+$/, "")}`;
|
|
130
|
+
/** One line for method run when the Method changed since its cases last passed. Cases with an author are named. */
|
|
129
131
|
export async function casesNotice(file) {
|
|
130
132
|
const cases = (await listCases(file)).filter((c) => c.status === "active");
|
|
131
133
|
if (!cases.length)
|
|
@@ -137,7 +139,33 @@ export async function casesNotice(file) {
|
|
|
137
139
|
catch { /* never checked */ }
|
|
138
140
|
if (checked?.passed && checked.fingerprint === methodFingerprint(file))
|
|
139
141
|
return undefined;
|
|
140
|
-
|
|
142
|
+
const named = cases.filter((c) => c.author);
|
|
143
|
+
return `${cases.length} case${cases.length === 1 ? "" : "s"} not checked since this Method changed${named.length ? ` (${named.map(caseName).join("; ")})` : ""}: method test ${file}`;
|
|
144
|
+
}
|
|
145
|
+
const signedIn = new Map();
|
|
146
|
+
/** The name of the signed-in person, for the author of a new case; null when signed out or offline. Asked once per sign-in in a process. */
|
|
147
|
+
export function signedInName(client = new MethodClient()) {
|
|
148
|
+
let token;
|
|
149
|
+
try {
|
|
150
|
+
token = client.token();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return Promise.resolve(null);
|
|
154
|
+
}
|
|
155
|
+
if (!token)
|
|
156
|
+
return Promise.resolve(null);
|
|
157
|
+
const key = `${client.server}\n${token}`;
|
|
158
|
+
if (!signedIn.has(key))
|
|
159
|
+
signedIn.set(key, (async () => {
|
|
160
|
+
try {
|
|
161
|
+
const me = await client.request("/api/me", "GET", undefined, true, { signal: AbortSignal.timeout(3000), stallMs: 3000 });
|
|
162
|
+
return typeof me?.user?.name === "string" && me.user.name.trim() ? me.user.name.trim() : null;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
})());
|
|
168
|
+
return signedIn.get(key);
|
|
141
169
|
}
|
|
142
170
|
/** The Method's declared secrets from this computer or the environment, as method run gives them. */
|
|
143
171
|
async function declaredSecrets(file) {
|
|
@@ -178,8 +206,10 @@ export async function caseGate(file, accept = [], reason) {
|
|
|
178
206
|
process.stderr.write(`Cases checked in ${Math.round(t.duration_ms / 1000)} s: ${t.live_model_steps} live model step${t.live_model_steps === 1 ? "" : "s"}, ${t.judge_calls} judge call${t.judge_calls === 1 ? "" : "s"}.\n`);
|
|
179
207
|
const failing = report.cases.filter((c) => !["pass", "fixed"].includes(c.verdict));
|
|
180
208
|
const unaccepted = failing.filter((c) => !accept.includes(c.id));
|
|
209
|
+
const authors = new Map(cases.map((c) => [c.id, c.author]));
|
|
210
|
+
const shown = (c) => authors.get(c.id) ? `${caseName({ ...c, author: authors.get(c.id) })} (${c.verdict})` : `${c.id} (${c.verdict}: ${c.note})`;
|
|
181
211
|
if (unaccepted.length)
|
|
182
|
-
throw Object.assign(Error(`This version breaks ${unaccepted.length} approved case${unaccepted.length === 1 ? "" : "s"}: ${unaccepted.map(
|
|
212
|
+
throw Object.assign(Error(`This version breaks ${unaccepted.length} approved case${unaccepted.length === 1 ? "" : "s"}: ${unaccepted.map(shown).join("; ")}. Fix the Method and run method test, or retire a case whose rule no longer applies with method case retire. To publish anyway, add --accept-failing-case ${unaccepted.map((c) => c.id).join(",")} with --reason.`), { code: "cases_failed" });
|
|
183
213
|
if (failing.length && !reason)
|
|
184
214
|
throw Error("Give --reason for accepting a failing case.");
|
|
185
215
|
return { report, line: failing.length ? `Cases: ${report.cases.length - failing.length} of ${report.cases.length} passed; accepted failing ${failing.map((c) => c.id).join(", ")}.` : `Cases: ${report.cases.length} of ${report.cases.length} passed.` };
|
|
@@ -214,7 +244,7 @@ export async function caseCommand(args) {
|
|
|
214
244
|
if (privacy)
|
|
215
245
|
process.stderr.write(privacy + "\n");
|
|
216
246
|
print(await createCase({ methodFile, runDir: values.run ? authoringPath(values.run) : undefined, passingRun: values["passing-run"] ? authoringPath(values["passing-run"]) : undefined,
|
|
217
|
-
id: values.id, note: values.note, author: values.author ??
|
|
247
|
+
id: values.id, note: values.note, author: values.author ?? await signedInName(), rubric: values.rubric ?? [], ref: values.ref, context: values.context ?? [], expect: json(values.expect) ?? [],
|
|
218
248
|
observations: json(values.observations), redact: json(values.redact), runs: integer(values.runs, "--runs"), minPass: integer(values["min-pass"], "--min-pass"),
|
|
219
249
|
retentionDays: integer(values["retention-days"], "--retention-days"), supersedes: values.supersedes ?? [], casesDir, acceptConflict: values["accept-conflict"], config: prepared.config,
|
|
220
250
|
options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } } }));
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { MethodClient } from './method-client.js';
|
|
2
2
|
import type { parse } from './local-cli.js';
|
|
3
|
+
/** How often a run that waits for the shared state checks again. Tests make it short. */
|
|
4
|
+
export declare const stateWait: {
|
|
5
|
+
pollMs: number;
|
|
6
|
+
};
|
|
3
7
|
/** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
|
|
4
8
|
export declare const finishedRun: (status: string) => status is "unconfirmed" | "completed";
|
|
5
9
|
export declare function runSaved(saved: any, flags: ReturnType<typeof parse>['values'], client: MethodClient): Promise<any>;
|
|
@@ -13,6 +13,37 @@ import { workerRunId } from './run-worker.js';
|
|
|
13
13
|
import { readDocument } from './authoring.js';
|
|
14
14
|
import { withInputsFrom } from './inputs-from.js';
|
|
15
15
|
import { setupFailure } from './failure.js';
|
|
16
|
+
/** How often a run that waits for the shared state checks again. Tests make it short. */
|
|
17
|
+
export const stateWait = { pollMs: 10_000 };
|
|
18
|
+
const clock = (at) => { const date = at ? new Date(at) : undefined; return date && !isNaN(+date) ? `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` : undefined; };
|
|
19
|
+
/**
|
|
20
|
+
* Take the shared state. While another run that still runs (or lost its connection) holds it, say so once and check
|
|
21
|
+
* again until it is free: there is no time limit, and method cancel stops the wait. A run that stopped while it held
|
|
22
|
+
* the state stops this one: a person must check that run's actions before releasing it.
|
|
23
|
+
*/
|
|
24
|
+
async function acquireState(client, statePath, runId, methodId) {
|
|
25
|
+
let told = false;
|
|
26
|
+
for (;;) {
|
|
27
|
+
try {
|
|
28
|
+
return await client.request(statePath, 'POST', { action: 'acquire', run_id: runId });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const owner = error?.body?.owner;
|
|
32
|
+
if (error?.code !== 'state_owned')
|
|
33
|
+
throw error;
|
|
34
|
+
if (owner && (owner.status === 'finished' || owner.status === 'failed')) {
|
|
35
|
+
const name = owner.started_by ?? 'Another person';
|
|
36
|
+
throw Object.assign(Error(`${name}'s run stopped while it held the shared state. ${name} must check its actions, then run \`method state ${methodId} --release ${owner.run_id}\`.`), { code: 'state_held', owner: 'setup' });
|
|
37
|
+
}
|
|
38
|
+
if (!told) {
|
|
39
|
+
const started = clock(owner?.started_at);
|
|
40
|
+
process.stderr.write(`Waiting for ${owner?.started_by ? `${owner.started_by}'s run` : 'another run'}${started ? ` (started ${started})` : ''} to finish with the shared state${owner?.link ? `: ${owner.link}` : '.'}\n`);
|
|
41
|
+
told = true;
|
|
42
|
+
}
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, stateWait.pollMs));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
16
47
|
// Older versions are listed only after their saved-package tests pass.
|
|
17
48
|
const supportedPackageRuntimes = new Set([runtimeVersion, '0.14.0', '0.12.0', '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
|
|
18
49
|
/** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
|
|
@@ -48,10 +79,17 @@ export async function runSaved(saved, flags, client) {
|
|
|
48
79
|
return completed;
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
|
-
flags = await withInputsFrom(flags, directory, undefined, client);
|
|
52
82
|
const inputFile = join(directory, 'requested-inputs.json');
|
|
53
|
-
|
|
54
|
-
|
|
83
|
+
// The start line already linked to this run: inputs that cannot be read fail it below, after its start is recorded.
|
|
84
|
+
let inputs = {}, unreadable;
|
|
85
|
+
try {
|
|
86
|
+
flags = await withInputsFrom(flags, directory, undefined, client);
|
|
87
|
+
inputs = flags.inputs ? JSON.parse(readFileSync(resolve(flags.inputs), 'utf8')) : existsSync(inputFile) ? JSON.parse(readFileSync(inputFile, 'utf8')) : {};
|
|
88
|
+
writePrivateJson(inputFile, inputs);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
unreadable = error;
|
|
92
|
+
}
|
|
55
93
|
const setupFile = join(directory, 'setup.json');
|
|
56
94
|
const setupEvents = existsSync(setupFile) ? JSON.parse(readFileSync(setupFile, 'utf8')) : [];
|
|
57
95
|
setupEvents.push({ at: new Date().toISOString(), type: 'setup_started' });
|
|
@@ -61,6 +99,8 @@ export async function runSaved(saved, flags, client) {
|
|
|
61
99
|
let acquired = false, started = false, revision = 0;
|
|
62
100
|
const statePath = `/api/cli/methods/${encodeURIComponent(saved.workflow_id)}/state`;
|
|
63
101
|
try {
|
|
102
|
+
if (unreadable)
|
|
103
|
+
throw unreadable;
|
|
64
104
|
const resuming = !!flags.resume && existsSync(join(directory, 'checkpoint.json'));
|
|
65
105
|
if (resuming)
|
|
66
106
|
assertCheckpointExecutor(JSON.parse(readFileSync(join(directory, 'checkpoint.json'), 'utf8')));
|
|
@@ -91,7 +131,7 @@ export async function runSaved(saved, flags, client) {
|
|
|
91
131
|
if (shared.enabled) {
|
|
92
132
|
if (flags.state)
|
|
93
133
|
throw Error('This Method uses account state. Do not replace it with --state.');
|
|
94
|
-
const state = await client
|
|
134
|
+
const state = await acquireState(client, statePath, request.run_id, saved.workflow_id);
|
|
95
135
|
acquired = true;
|
|
96
136
|
revision = state.revision;
|
|
97
137
|
const pendingFile = join(directory, 'state-pending.json');
|
|
@@ -116,7 +156,7 @@ export async function runSaved(saved, flags, client) {
|
|
|
116
156
|
const result = await client.request(statePath, 'POST', commit);
|
|
117
157
|
revision = result.revision;
|
|
118
158
|
}
|
|
119
|
-
}, client, config);
|
|
159
|
+
}, client, config, {}, saved.saved_by ?? null);
|
|
120
160
|
// Every step finished (an unconfirmed run too), so the run no longer owns the shared state.
|
|
121
161
|
if (acquired && finishedRun(result.status)) {
|
|
122
162
|
await client.request(statePath, 'POST', { action: 'release', run_id: request.run_id });
|
|
@@ -6,7 +6,7 @@ import { loadWorkflow, parseDocumentValue, serializeWorkflow } from '../../workf
|
|
|
6
6
|
import { buildPackage } from './method-files.js';
|
|
7
7
|
import { authoringPath, readDocument } from './authoring.js';
|
|
8
8
|
import { Outbox, lastSaved, savedVersions } from './outbox.js';
|
|
9
|
-
import { claimHome
|
|
9
|
+
import { claimHome } from './home.js';
|
|
10
10
|
export const methodIdPattern = /^wf_[a-f0-9]{32}$/;
|
|
11
11
|
/** A new account Method ID, made on this computer. The first save of this ID creates the Method. */
|
|
12
12
|
export const newMethodId = () => `wf_${randomBytes(16).toString('hex')}`;
|
|
@@ -98,7 +98,6 @@ export async function prepareVersion(client, outbox, file, reason) {
|
|
|
98
98
|
const { pack, sources, tooLarge } = await buildPackage(file, workflow);
|
|
99
99
|
const versionId = versionIdFor(id, pack.digest);
|
|
100
100
|
const url = `${client.server}/methods/${id}?version=${versionId}`;
|
|
101
|
-
recordHome(client.server, id, file, { version_id: versionId, files: writtenFiles(file, pack.files) });
|
|
102
101
|
if (savedVersions(client.server)[versionId])
|
|
103
102
|
return { method_id: id, version_id: versionId, url, unchanged: true, workflow };
|
|
104
103
|
const previous = lastSaved(client.server, id);
|
|
@@ -107,7 +106,8 @@ export async function prepareVersion(client, outbox, file, reason) {
|
|
|
107
106
|
for (const f of pack.files)
|
|
108
107
|
outbox.add({ kind: 'file', server: client.server, sha256: f.sha256, size: f.size }, sources[f.sha256]);
|
|
109
108
|
outbox.add({ kind: 'version', server: client.server, method_id: id, version_id: versionId, file: authoringPath(file), body: {
|
|
110
|
-
workflow, package: pack, ...(previous && previous.version_id !== versionId ? { parent_version_id: previous.version_id } : {}),
|
|
109
|
+
workflow, package: pack, ...(previous?.version_id && previous.version_id !== versionId ? { parent_version_id: previous.version_id } : {}),
|
|
110
|
+
...(previous?.merged_version_id && previous.merged_version_id !== versionId ? { merged_version_id: previous.merged_version_id } : {}),
|
|
111
111
|
reason: reason ?? changeReason(previous?.workflow, workflow),
|
|
112
112
|
} });
|
|
113
113
|
return { method_id: id, version_id: versionId, url, unchanged: false, workflow };
|
|
@@ -115,6 +115,8 @@ export async function prepareVersion(client, outbox, file, reason) {
|
|
|
115
115
|
/** The one line a run prints about its version. */
|
|
116
116
|
export function savedLine(client, outbox, version) {
|
|
117
117
|
const known = savedVersions(client.server)[version.version_id];
|
|
118
|
+
if (known?.beside !== undefined)
|
|
119
|
+
return `Saved beside ${known.beside ? `${known.beside}'s` : 'a'} newer version; your next run combines them.`;
|
|
118
120
|
if (known)
|
|
119
121
|
return `Saved as version ${known.version_number}: ${version.url}`;
|
|
120
122
|
const failure = outbox.failure(version.version_id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@method-ai-hq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Build and run reusable .method files with checked steps and saved results.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -157,6 +157,11 @@
|
|
|
157
157
|
"import": "./dist/packages/sdk/src/versions.js",
|
|
158
158
|
"default": "./dist/packages/sdk/src/versions.js"
|
|
159
159
|
},
|
|
160
|
+
"./merge": {
|
|
161
|
+
"types": "./dist/packages/sdk/src/merge.d.ts",
|
|
162
|
+
"import": "./dist/packages/sdk/src/merge.js",
|
|
163
|
+
"default": "./dist/packages/sdk/src/merge.js"
|
|
164
|
+
},
|
|
160
165
|
"./outbox": {
|
|
161
166
|
"types": "./dist/packages/sdk/src/outbox.d.ts",
|
|
162
167
|
"import": "./dist/packages/sdk/src/outbox.js",
|