@method-ai-hq/sdk 0.17.3 → 0.18.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 +2 -2
- package/dist/packages/sdk/package.json +2 -2
- package/dist/packages/sdk/src/authoring-instructions.d.ts +4 -4
- package/dist/packages/sdk/src/authoring-instructions.js +5 -5
- package/dist/packages/sdk/src/authoring.js +8 -6
- package/dist/packages/sdk/src/capabilities.d.ts +9 -6
- package/dist/packages/sdk/src/capabilities.js +15 -32
- package/dist/packages/sdk/src/computer-settings.d.ts +2 -5
- package/dist/packages/sdk/src/computer-settings.js +4 -11
- package/dist/packages/sdk/src/current-runtime.js +11 -6
- package/dist/packages/sdk/src/improve.d.ts +104 -29
- package/dist/packages/sdk/src/improve.js +240 -70
- package/dist/packages/sdk/src/judge.d.ts +12 -4
- package/dist/packages/sdk/src/judge.js +46 -10
- package/dist/packages/sdk/src/local-cli.d.ts +0 -1
- package/dist/packages/sdk/src/local-cli.js +16 -15
- package/dist/packages/sdk/src/local-judgments.d.ts +40 -0
- package/dist/packages/sdk/src/local-judgments.js +144 -0
- package/dist/packages/sdk/src/local-setup.d.ts +0 -2
- package/dist/packages/sdk/src/local-setup.js +1 -1
- package/dist/packages/sdk/src/method-help.d.ts +1 -1
- package/dist/packages/sdk/src/method-help.js +20 -23
- package/dist/packages/sdk/src/method.js +35 -2
- package/dist/packages/sdk/src/model-call.d.ts +73 -0
- package/dist/packages/sdk/src/model-call.js +74 -0
- package/dist/packages/sdk/src/own-model-key.d.ts +1 -1
- package/dist/packages/sdk/src/own-model-key.js +1 -1
- package/dist/packages/sdk/src/proposals.d.ts +10 -26
- package/dist/packages/sdk/src/proposals.js +32 -64
- package/dist/packages/sdk/src/quality.d.ts +1 -2
- package/dist/packages/sdk/src/quality.js +25 -16
- package/package.json +2 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { methodCache } from './prepare.js';
|
|
5
|
+
/** A Method type for a JSON schema: object → record, array → list, string → text. */
|
|
6
|
+
export function methodType(schema) {
|
|
7
|
+
const described = (value) => schema.description || schema.enum ? { ...value, description: [schema.description, schema.enum ? `One of: ${schema.enum.join(', ')}.` : ''].filter(Boolean).join(' ') } : value;
|
|
8
|
+
if (schema.type === 'object')
|
|
9
|
+
return described({ type: 'record', fields: fieldsOf(schema) });
|
|
10
|
+
if (schema.type === 'array')
|
|
11
|
+
return described(schema.items?.type === 'object' ? { type: 'list', fields: fieldsOf(schema.items) } : { type: 'list', items: methodType(schema.items ?? { type: 'string' }) });
|
|
12
|
+
if (schema.type === 'number' || schema.type === 'integer')
|
|
13
|
+
return described({ type: 'number' });
|
|
14
|
+
if (schema.type === 'boolean')
|
|
15
|
+
return described({ type: 'boolean' });
|
|
16
|
+
return described({ type: 'text' });
|
|
17
|
+
}
|
|
18
|
+
const fieldsOf = (schema) => Object.fromEntries(Object.entries(schema.properties ?? {}).map(([name, value]) => [name, methodType(value)]));
|
|
19
|
+
/** The one-step Method of a request. Instructions with prompt braces go in the inputs, so nothing in them is read as a variable. */
|
|
20
|
+
export function requestMethod(request) {
|
|
21
|
+
if (request.schema.type !== 'object')
|
|
22
|
+
throw Error('A model request asks for a JSON object.');
|
|
23
|
+
const braces = /\{\{|\}\}/.test(request.system);
|
|
24
|
+
const model = request.model ?? 'default';
|
|
25
|
+
const fields = fieldsOf(request.schema);
|
|
26
|
+
return {
|
|
27
|
+
format: 'method/3.4', name: 'Model request', goal: 'Answer one request.',
|
|
28
|
+
...(request.models ? { models: request.models } : {}),
|
|
29
|
+
inputs: { request: { type: 'text', description: 'The request.' }, ...(braces ? { instructions: { type: 'text', description: 'The instructions.' } } : {}) },
|
|
30
|
+
steps: { answer: { name: 'Answer', in: { request: 'inputs.request', ...(braces ? { instructions: 'inputs.instructions' } : {}) },
|
|
31
|
+
do: { kind: 'call', model, prompt: braces ? 'Follow the instructions in the step inputs, then answer the request.' : request.system }, out: fields } },
|
|
32
|
+
result: Object.fromEntries(Object.keys(fields).map(name => [name, name])),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Run one model request through the runtime, with the preparation of method run. */
|
|
36
|
+
export async function callModel(request) {
|
|
37
|
+
const dir = join(methodCache(), 'model-requests', randomUUID());
|
|
38
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
const file = join(dir, 'request.method');
|
|
41
|
+
writeFileSync(file, JSON.stringify(requestMethod(request)), { mode: 0o600 });
|
|
42
|
+
const { preparedConfig } = await import('./quality.js');
|
|
43
|
+
const { runMethod } = await import('@withmethod/runtime/runner.js');
|
|
44
|
+
const prepared = await preparedConfig(file, {});
|
|
45
|
+
const started = performance.now();
|
|
46
|
+
const result = await runMethod(file, prepared.config, { runDir: join(dir, 'run'), inputs: { request: request.user, ...(/\{\{|\}\}/.test(request.system) ? { instructions: request.system } : {}) },
|
|
47
|
+
processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) });
|
|
48
|
+
if (result.status !== 'completed')
|
|
49
|
+
throw Object.assign(Error(result.failure?.headline ?? `The model request ended with status ${result.status}.`), { code: result.failure?.code ?? 'model_failed', owner: result.failure?.owner });
|
|
50
|
+
const profile = prepared.config.models?.[request.model ?? 'default'];
|
|
51
|
+
const hosted = profile?.backend === 'method';
|
|
52
|
+
return { value: result.result, agent: hosted ? `hosted:${profile.model}` : profile?.backend ?? 'unknown', usd: hosted && typeof result.usage?.cost_usd === 'number' ? result.usage.cost_usd : null,
|
|
53
|
+
duration_ms: Math.round(performance.now() - started) };
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `method __model`: one model request from a script of a Method (improve.method), as JSON on stdin; the answer as
|
|
61
|
+
* JSON on stdout. A failure prints {error, code, owner} and exits 1.
|
|
62
|
+
*/
|
|
63
|
+
export async function modelCallCommand() {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
for await (const chunk of process.stdin)
|
|
66
|
+
chunks.push(chunk);
|
|
67
|
+
try {
|
|
68
|
+
process.stdout.write(JSON.stringify(await callModel(JSON.parse(Buffer.concat(chunks).toString('utf8')))) + '\n');
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
process.stdout.write(JSON.stringify({ error: error.message, code: error.code ?? 'model_failed', owner: error.owner ?? null }) + '\n');
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HostedModels } from '@withmethod/runtime';
|
|
2
2
|
export declare const ownKeyName = "OPENROUTER_API_KEY";
|
|
3
|
-
/** The name of this computer's own model key, or undefined.
|
|
3
|
+
/** The name of this computer's own model key, or undefined. */
|
|
4
4
|
export declare const ownKeyEnv: () => string | undefined;
|
|
5
5
|
/** method config model-key [--off]: open the private form for the key, then use it for model and classify steps. */
|
|
6
6
|
export declare function modelKeyCommand(args: string[], set?: (name: string) => Promise<unknown>): Promise<void>;
|
|
@@ -7,7 +7,7 @@ import { typesafeModel } from '@withmethod/runtime/classification.js';
|
|
|
7
7
|
import { readComputerSettings, updateComputerSettings } from './computer-settings.js';
|
|
8
8
|
import { findSecrets, importSecrets, resolveSecrets, setSecret } from './secrets.js';
|
|
9
9
|
export const ownKeyName = 'OPENROUTER_API_KEY';
|
|
10
|
-
/** The name of this computer's own model key, or undefined.
|
|
10
|
+
/** The name of this computer's own model key, or undefined. */
|
|
11
11
|
export const ownKeyEnv = () => readComputerSettings().model_key_env;
|
|
12
12
|
/** method config model-key [--off]: open the private form for the key, then use it for model and classify steps. */
|
|
13
13
|
export async function modelKeyCommand(args, set = setSecret) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MethodClient } from './method-client.js';
|
|
2
|
-
/** A
|
|
2
|
+
/** A change to a Method: from the account (a change Method made there), or from the improvement loop on this computer. */
|
|
3
3
|
export type ProposalRecord = {
|
|
4
4
|
id: string;
|
|
5
5
|
source: 'account' | 'local';
|
|
@@ -33,34 +33,19 @@ export type MergeResult = {
|
|
|
33
33
|
export declare function mergeMethod(base: any, ours: any, theirs: any): MergeResult;
|
|
34
34
|
/** The steps whose definition differs between two documents. */
|
|
35
35
|
export declare function changedSteps(before: any, after: any): string[];
|
|
36
|
+
/** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
|
|
37
|
+
export declare function writeMethodFile(file: string, id: string | undefined, content: any): void;
|
|
36
38
|
export declare const localProposalFolder: (file: string) => string;
|
|
37
|
-
/**
|
|
39
|
+
/** Record what the improvement loop on this computer did (applied, or none) as a private file in the .method folder: it can hold run content. */
|
|
38
40
|
export declare function saveLocalProposal(file: string, record: Omit<ProposalRecord, 'source'> & Record<string, unknown>): string;
|
|
39
41
|
export declare function localProposals(file: string): (ProposalRecord & {
|
|
40
42
|
path: string;
|
|
41
43
|
})[];
|
|
42
|
-
type RequestOptions = {
|
|
43
|
-
signal?: AbortSignal;
|
|
44
|
-
stallMs?: number;
|
|
45
|
-
};
|
|
46
|
-
type Applied = {
|
|
47
|
-
status: 'applied';
|
|
48
|
-
steps: string[];
|
|
49
|
-
version_id: string | undefined;
|
|
50
|
-
warning?: string;
|
|
51
|
-
} | {
|
|
52
|
-
status: 'conflict';
|
|
53
|
-
conflicts: MergeConflict[];
|
|
54
|
-
} | {
|
|
55
|
-
status: 'invalid';
|
|
56
|
-
error: string;
|
|
57
|
-
};
|
|
58
|
-
/** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
|
|
59
|
-
export declare function applyProposal(client: MethodClient | undefined, file: string, proposal: ProposalRecord, options?: RequestOptions): Promise<Applied>;
|
|
60
44
|
/**
|
|
61
|
-
* Before a command that takes a .method FILE: when this computer is signed in and Method
|
|
62
|
-
* (
|
|
63
|
-
*
|
|
45
|
+
* Before a command that takes a .method FILE: when this computer is signed in and Method changed the Method in the
|
|
46
|
+
* account (keeping run data in the account, or a fix of a Method that had not run on a computer), merge the change into
|
|
47
|
+
* the file and print one line. A conflict leaves the file unchanged and writes both versions of each conflicting part
|
|
48
|
+
* to a file for the coding agent to merge; the command goes on. Offline, signed out, or on any error, it does nothing.
|
|
64
49
|
*/
|
|
65
50
|
export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
66
51
|
/** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
|
|
@@ -68,6 +53,5 @@ export declare function localRuns(file: string): Promise<{
|
|
|
68
53
|
dir: string;
|
|
69
54
|
account_run_id: string | null;
|
|
70
55
|
}[]>;
|
|
71
|
-
/** `method
|
|
72
|
-
export declare function
|
|
73
|
-
export {};
|
|
56
|
+
/** `method changes FILE [--json]`. */
|
|
57
|
+
export declare function changesMain(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
|
|
@@ -6,6 +6,7 @@ import { authoringPath, readDocument, writeDocument } from './authoring.js';
|
|
|
6
6
|
import { DEFAULT_SERVER } from './method-client.js';
|
|
7
7
|
import { writePrivateJson } from './files.js';
|
|
8
8
|
import { currentVersion, methodContent, methodIdPattern } from './versions.js';
|
|
9
|
+
import { localMethodFolder } from './local-judgments.js';
|
|
9
10
|
const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
10
11
|
/** Keys of ours in their order; a key that only theirs has goes after the key before it in theirs. */
|
|
11
12
|
function keyOrder(ours, theirs) {
|
|
@@ -56,7 +57,7 @@ export function changedSteps(before, after) {
|
|
|
56
57
|
return [...new Set([...Object.keys(before?.steps ?? {}), ...Object.keys(after?.steps ?? {})])].filter(id => !isDeepStrictEqual(before?.steps?.[id], after?.steps?.[id]));
|
|
57
58
|
}
|
|
58
59
|
/** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
|
|
59
|
-
function writeMethodFile(file, id, content) {
|
|
60
|
+
export function writeMethodFile(file, id, content) {
|
|
60
61
|
const { format, ...rest } = content;
|
|
61
62
|
const document = { ...(format !== undefined ? { format } : {}), ...(id ? { id } : {}), ...rest };
|
|
62
63
|
if (/^\s*\{/.test(readFileSync(file, 'utf8'))) {
|
|
@@ -74,13 +75,11 @@ function writeMethodFile(file, id, content) {
|
|
|
74
75
|
writeDocument(file, document);
|
|
75
76
|
}
|
|
76
77
|
export const localProposalFolder = (file) => join(dirname(file), '.method', 'proposals');
|
|
77
|
-
/**
|
|
78
|
+
/** Record what the improvement loop on this computer did (applied, or none) as a private file in the .method folder: it can hold run content. */
|
|
78
79
|
export function saveLocalProposal(file, record) {
|
|
79
|
-
|
|
80
|
-
const path = join(
|
|
80
|
+
localMethodFolder(file);
|
|
81
|
+
const path = join(localProposalFolder(file), `${record.id}.json`);
|
|
81
82
|
writePrivateJson(path, { ...record, source: 'local', file: basename(file) });
|
|
82
|
-
if (!existsSync(ignore))
|
|
83
|
-
writeFileSync(ignore, '*\n', { mode: 0o600 });
|
|
84
83
|
return path;
|
|
85
84
|
}
|
|
86
85
|
export function localProposals(file) {
|
|
@@ -101,8 +100,8 @@ async function newestAccepted(client, methodId, options = {}) {
|
|
|
101
100
|
const accepted = (proposals ?? []).filter(p => p?.status === 'accepted').sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
|
|
102
101
|
return accepted[0] ? accountProposal(client, accepted[0].id, options) : undefined;
|
|
103
102
|
}
|
|
104
|
-
/** Merge a
|
|
105
|
-
|
|
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 = {}) {
|
|
106
105
|
const document = readDocument(file), ours = methodContent(document);
|
|
107
106
|
const merged = mergeMethod(proposal.base_workflow, ours, proposal.proposed_workflow);
|
|
108
107
|
if (!merged.clean)
|
|
@@ -119,11 +118,6 @@ export async function applyProposal(client, file, proposal, options = {}) {
|
|
|
119
118
|
}
|
|
120
119
|
async function markApplied(client, file, proposal, options = {}) {
|
|
121
120
|
const version_id = (await currentVersion(file).catch(() => undefined))?.version_id;
|
|
122
|
-
if (proposal.source === 'local') {
|
|
123
|
-
const path = join(localProposalFolder(file), `${proposal.id}.json`), record = JSON.parse(readFileSync(path, 'utf8'));
|
|
124
|
-
writePrivateJson(path, { ...record, status: 'applied', applied_at: new Date().toISOString(), ...(version_id ? { applied_version_id: version_id } : {}) });
|
|
125
|
-
return { version_id };
|
|
126
|
-
}
|
|
127
121
|
try {
|
|
128
122
|
await client.request(`/api/proposals/${encodeURIComponent(proposal.id)}/applied`, 'POST', version_id ? { version_id } : {}, true, options);
|
|
129
123
|
return { version_id };
|
|
@@ -133,11 +127,12 @@ async function markApplied(client, file, proposal, options = {}) {
|
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
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]; };
|
|
136
|
-
const skipAutoApply = new Set(['
|
|
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']);
|
|
137
131
|
/**
|
|
138
|
-
* Before a command that takes a .method FILE: when this computer is signed in and Method
|
|
139
|
-
* (
|
|
140
|
-
*
|
|
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.
|
|
141
136
|
*/
|
|
142
137
|
export async function autoApply(args, clientFactory) {
|
|
143
138
|
if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
|
|
@@ -160,13 +155,18 @@ export async function autoApply(args, clientFactory) {
|
|
|
160
155
|
if (result.status === 'applied') {
|
|
161
156
|
if (result.steps.length || result.warning)
|
|
162
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;
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
168
|
}
|
|
169
|
-
catch { /* offline or signed out: the command goes on without the
|
|
169
|
+
catch { /* offline or signed out: the command goes on without the change. */ }
|
|
170
170
|
}
|
|
171
171
|
/** 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
172
|
export async function localRuns(file) {
|
|
@@ -186,7 +186,7 @@ export async function localRuns(file) {
|
|
|
186
186
|
}
|
|
187
187
|
const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
188
188
|
const changeLine = (change) => `${change.date.slice(0, 10)} ${change.version_id ?? 'not saved as a version'} ${change.cause}`;
|
|
189
|
-
/** `method changes FILE`: the changes that Method applied
|
|
189
|
+
/** `method changes FILE`: the changes that Method applied in the account and on this computer, newest first. */
|
|
190
190
|
async function changesCommand(file, target, methodId, client, json) {
|
|
191
191
|
const local = localProposals(file).filter(p => p.status === 'applied').map((p) => ({ id: p.id, source: 'local', cause: p.cause, date: String(p.applied_at ?? p.created_at), version_id: p.applied_version_id ?? null, steps: p.evidence?.steps ?? [] }));
|
|
192
192
|
let account = [];
|
|
@@ -201,48 +201,16 @@ async function changesCommand(file, target, methodId, client, json) {
|
|
|
201
201
|
return print({ file, method_id: methodId, changes });
|
|
202
202
|
process.stdout.write(changes.length ? changes.map(changeLine).join('\n') + '\n' : `Method has not changed ${target} yet.\n`);
|
|
203
203
|
}
|
|
204
|
-
/** `method
|
|
205
|
-
export async function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!target || positionals.length > (args[0] === 'apply' ? 2 : 1))
|
|
211
|
-
throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method changes FILE [--json].');
|
|
204
|
+
/** `method changes FILE [--json]`. */
|
|
205
|
+
export async function changesMain(args, clientFactory) {
|
|
206
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { server: { type: 'string' }, json: { type: 'boolean' } } });
|
|
207
|
+
const [target] = positionals;
|
|
208
|
+
if (!target || positionals.length !== 1)
|
|
209
|
+
throw Error('Use method changes FILE [--json].');
|
|
212
210
|
const file = authoringPath(target), id = readDocument(file).id;
|
|
213
211
|
const methodId = typeof id === 'string' && methodIdPattern.test(id) ? id : null;
|
|
214
212
|
const client = clientFactory(values.server ?? DEFAULT_SERVER);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const local = localProposals(file);
|
|
219
|
-
let proposal = wanted ? local.find(p => p.id === wanted) : undefined;
|
|
220
|
-
if (!proposal && wanted) {
|
|
221
|
-
if (!signedIn)
|
|
222
|
-
await client.login();
|
|
223
|
-
proposal = await accountProposal(client, wanted);
|
|
224
|
-
if (methodId && proposal.method_id !== methodId)
|
|
225
|
-
throw Error(`Proposal ${wanted} is for another Method.`);
|
|
226
|
-
}
|
|
227
|
-
proposal ??= local.find(p => p.status === 'open');
|
|
228
|
-
if (!proposal)
|
|
229
|
-
throw Error(`${target} has no open proposal. Run method improve ${target} --note TEXT first.`);
|
|
230
|
-
if (proposal.status === 'applied')
|
|
231
|
-
return print({ status: 'applied', file, proposal_id: proposal.id, next: 'This proposal is already applied.' });
|
|
232
|
-
if (proposal.source === 'account' && proposal.status !== 'accepted')
|
|
233
|
-
throw Error(`Change ${proposal.id} is ${proposal.status}, so it has nothing to merge.`);
|
|
234
|
-
if (proposal.source === 'local' && proposal.status !== 'open')
|
|
235
|
-
throw Error(`Proposal ${proposal.id} is ${proposal.status}.`);
|
|
236
|
-
if (values.resolved)
|
|
237
|
-
return print({ status: 'applied', file, proposal_id: proposal.id, ...await markApplied(client, file, proposal), next: `Run method test ${target}. Publish it when it is right.` });
|
|
238
|
-
if (!proposal.proposed_workflow)
|
|
239
|
-
return print({ status: 'none', file, proposal_id: proposal.id, cause: proposal.cause, next: 'No change passed the tests; there is nothing to apply.' });
|
|
240
|
-
const result = await applyProposal(client, file, proposal);
|
|
241
|
-
if (result.status === 'applied')
|
|
242
|
-
return print({ ...result, file, proposal_id: proposal.id, cause: proposal.cause, next: `Run method test ${target}. Publish it when it is right.` });
|
|
243
|
-
process.exitCode = 1;
|
|
244
|
-
if (result.status === 'invalid')
|
|
245
|
-
return print({ status: 'invalid', file, proposal_id: proposal.id, error: result.error, next: 'The file is not changed. Merge the proposed version by hand.' });
|
|
246
|
-
print({ status: 'conflict', file, proposal_id: proposal.id, cause: proposal.cause, conflicts: result.conflicts,
|
|
247
|
-
next: `The file is not changed. Your edits and the proposal both change each listed part. Edit ${target} so that each part keeps both changes, then run method apply ${target} ${proposal.id} --resolved.` });
|
|
213
|
+
if (methodId && !client.token())
|
|
214
|
+
await client.login();
|
|
215
|
+
return changesCommand(file, target, methodId, client, !!values.json);
|
|
248
216
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** The same runtime preparation as method run, without starting a run. Hosted models are used when signed in. */
|
|
2
2
|
export declare function preparedConfig(file: string, flags: {
|
|
3
3
|
workspace?: string;
|
|
4
|
-
agent?: string;
|
|
5
4
|
}, options?: {
|
|
6
5
|
judge?: boolean;
|
|
7
6
|
}): Promise<{
|
|
@@ -22,12 +21,12 @@ export declare function casesNotice(file: string): Promise<string | undefined>;
|
|
|
22
21
|
/** Run the cases of a Method with the same preparation as method run. */
|
|
23
22
|
export declare function runCases(file: string, flags: {
|
|
24
23
|
workspace?: string;
|
|
25
|
-
agent?: string;
|
|
26
24
|
}, options?: {
|
|
27
25
|
casesDir?: string | undefined;
|
|
28
26
|
ids?: string[] | undefined;
|
|
29
27
|
baseline?: string | undefined;
|
|
30
28
|
newIds?: string[];
|
|
29
|
+
once?: boolean;
|
|
31
30
|
}): Promise<{
|
|
32
31
|
counts: {
|
|
33
32
|
[k: string]: number;
|
|
@@ -10,6 +10,7 @@ import { createCase, retireCase, listCases, testSuite } from "@withmethod/runtim
|
|
|
10
10
|
import { authoringPath, editDocument, readDocument } from "./authoring.js";
|
|
11
11
|
import { localSetup } from "./local-setup.js";
|
|
12
12
|
import { resolveAgentProfiles, checkAgents, accountNeeds } from "./capabilities.js";
|
|
13
|
+
import { localAgent, resolveModels } from "@withmethod/runtime/agents.js";
|
|
13
14
|
import { hostedModelsFor, managedModels } from "./hosted-models.js";
|
|
14
15
|
import { managedClassification } from "./classification-client.js";
|
|
15
16
|
import { MethodClient } from "./method-client.js";
|
|
@@ -30,21 +31,29 @@ export async function preparedConfig(file, flags, options = {}) {
|
|
|
30
31
|
catch {
|
|
31
32
|
return false;
|
|
32
33
|
} })();
|
|
33
|
-
// A rubric judge is a model step too:
|
|
34
|
-
const judge = options.judge && !
|
|
35
|
-
|
|
34
|
+
// A rubric judge is a model step too: it follows the same rule (the user's own local agent, else hosted).
|
|
35
|
+
const judge = options.judge && !config.models?.judge;
|
|
36
|
+
// The hosted model is asked for only when no local agent can run the model steps.
|
|
37
|
+
if ((accountNeeds(method, config).models || judge) && signedIn && !await localAgent()) {
|
|
36
38
|
try {
|
|
37
39
|
hostedModel = await managedModels(client).model();
|
|
38
40
|
}
|
|
39
41
|
catch (error) {
|
|
40
|
-
process.stderr.write(`Hosted models are unavailable (${error.message})
|
|
42
|
+
process.stderr.write(`Hosted models are unavailable (${error.message}).\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
config.models = await resolveAgentProfiles(method, config, hostedModel);
|
|
46
|
+
if (judge) {
|
|
47
|
+
try {
|
|
48
|
+
config.models.judge = (await resolveModels({ steps: { judge: { do: { kind: 'call', model: 'judge' } } } }, {}, { hostedModel })).judge;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== 'needs_input')
|
|
52
|
+
throw error;
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
|
-
config.models = await resolveAgentProfiles(method, config, setup.agent, hostedModel);
|
|
44
|
-
if (judge && hostedModel)
|
|
45
|
-
config.models.judge = { backend: "method", model: hostedModel };
|
|
46
55
|
// Classify steps that run live use the account's classifier, as method run does.
|
|
47
|
-
const classification = accountNeeds(method, config
|
|
56
|
+
const classification = accountNeeds(method, config).classification && signedIn ? managedClassification(client) : undefined;
|
|
48
57
|
if (classification)
|
|
49
58
|
config.classification = await classification.resolve(new AbortController().signal);
|
|
50
59
|
await checkAgents(config.models);
|
|
@@ -139,18 +148,18 @@ async function declaredSecrets(file) {
|
|
|
139
148
|
export async function runCases(file, flags, options = {}) {
|
|
140
149
|
const prepared = await preparedConfig(file, flags, { judge: true });
|
|
141
150
|
const report = await testSuite(file, prepared.config, { ...options,
|
|
142
|
-
runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {})
|
|
143
|
-
if (!options.ids && !options.casesDir)
|
|
151
|
+
runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } });
|
|
152
|
+
if (!options.ids && !options.casesDir && !options.once)
|
|
144
153
|
recordChecked(file, report);
|
|
145
154
|
return report;
|
|
146
155
|
}
|
|
147
156
|
export async function testCommand(args) {
|
|
148
|
-
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" },
|
|
157
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, once: { type: "boolean" } } });
|
|
149
158
|
const file = positionals[0];
|
|
150
159
|
if (!file || positionals.length !== 1)
|
|
151
|
-
throw Error("Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]...");
|
|
160
|
+
throw Error("Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--once]");
|
|
152
161
|
const report = await runCases(authoringPath(file), values, { casesDir: values.cases ? authoringPath(values.cases) : undefined, ids: values.case,
|
|
153
|
-
baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [] });
|
|
162
|
+
baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [], once: !!values.once });
|
|
154
163
|
print(report);
|
|
155
164
|
process.exitCode = report.passed ? 0 : 1;
|
|
156
165
|
}
|
|
@@ -178,7 +187,7 @@ export async function caseGate(file, accept = [], reason) {
|
|
|
178
187
|
export async function caseCommand(args) {
|
|
179
188
|
const text = { type: "string" };
|
|
180
189
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { run: text, "passing-run": text, id: text, note: text, rubric: { type: "string", multiple: true }, ref: text, context: { type: "string", multiple: true }, expect: text,
|
|
181
|
-
observations: text, redact: text, runs: text, "min-pass": text, author: text, reason: text, by: text, cases: text, workspace: text,
|
|
190
|
+
observations: text, redact: text, runs: text, "min-pass": text, author: text, reason: text, by: text, cases: text, workspace: text,
|
|
182
191
|
"retention-days": text, supersedes: { type: "string", multiple: true }, "accept-conflict": text } });
|
|
183
192
|
const [action, file, id] = positionals;
|
|
184
193
|
if (!file || !["new", "retire", "list"].includes(action ?? ""))
|
|
@@ -200,7 +209,7 @@ export async function caseCommand(args) {
|
|
|
200
209
|
throw Error('Supply --id, --note "the person\'s words", and --run (the run that went wrong) or --passing-run (a run that was right).');
|
|
201
210
|
if (!values.rubric?.length && !values.expect)
|
|
202
211
|
throw Error('Say what must be true with --rubric "plain sentence" (repeatable), or give --expect FILE.');
|
|
203
|
-
const prepared = await preparedConfig(methodFile, { ...(values.workspace ? { workspace: values.workspace } : {})
|
|
212
|
+
const prepared = await preparedConfig(methodFile, { ...(values.workspace ? { workspace: values.workspace } : {}) }, { judge: true });
|
|
204
213
|
const privacy = casesPrivacyWarning(methodFile, casesDir);
|
|
205
214
|
if (privacy)
|
|
206
215
|
process.stderr.write(privacy + "\n");
|
|
@@ -208,7 +217,7 @@ export async function caseCommand(args) {
|
|
|
208
217
|
id: values.id, note: values.note, author: values.author ?? null, rubric: values.rubric ?? [], ref: values.ref, context: values.context ?? [], expect: json(values.expect) ?? [],
|
|
209
218
|
observations: json(values.observations), redact: json(values.redact), runs: integer(values.runs, "--runs"), minPass: integer(values["min-pass"], "--min-pass"),
|
|
210
219
|
retentionDays: integer(values["retention-days"], "--retention-days"), supersedes: values.supersedes ?? [], casesDir, acceptConflict: values["accept-conflict"], config: prepared.config,
|
|
211
|
-
options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {})
|
|
220
|
+
options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } } }));
|
|
212
221
|
}
|
|
213
222
|
/**
|
|
214
223
|
* A case copies run data (inputs, outputs, written files) into cases/. When that folder would be committed to a Git
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@method-ai-hq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Build and run reusable .method files with checked steps and saved results.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"prepack": "npm --prefix ../.. run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/
|
|
38
|
+
"@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/v0.15.0",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"yaml": "^2.9.0",
|
|
41
41
|
"zod": "^3.25.76"
|