@method-ai-hq/sdk 0.19.0 → 0.20.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 +2 -2
- package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
- package/dist/packages/sdk/src/authoring-instructions.js +1 -1
- package/dist/packages/sdk/src/current-inspection.js +3 -0
- package/dist/packages/sdk/src/fix-lines.d.ts +82 -0
- package/dist/packages/sdk/src/fix-lines.js +63 -0
- package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
- package/dist/packages/sdk/src/fix-outcomes.js +201 -0
- package/dist/packages/sdk/src/improve.d.ts +131 -34
- package/dist/packages/sdk/src/improve.js +489 -175
- package/dist/packages/sdk/src/judge.d.ts +12 -11
- package/dist/packages/sdk/src/judge.js +63 -39
- package/dist/packages/sdk/src/learned.d.ts +43 -0
- package/dist/packages/sdk/src/learned.js +97 -0
- package/dist/packages/sdk/src/local-cli.js +8 -1
- package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
- package/dist/packages/sdk/src/method-files.d.ts +3 -2
- package/dist/packages/sdk/src/method-files.js +7 -3
- package/dist/packages/sdk/src/method-help.d.ts +1 -1
- package/dist/packages/sdk/src/method-help.js +16 -12
- package/dist/packages/sdk/src/method.js +51 -9
- package/dist/packages/sdk/src/quality.d.ts +62 -11
- package/dist/packages/sdk/src/quality.js +56 -21
- package/dist/packages/sdk/src/undo.d.ts +50 -0
- package/dist/packages/sdk/src/undo.js +108 -0
- package/dist/packages/sdk/src/watch.d.ts +20 -0
- package/dist/packages/sdk/src/watch.js +86 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
- package/dist/packages/workflow-language/src/inspection.js +3 -1
- package/package.json +2 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type MethodClient } from './method-client.js';
|
|
2
|
-
import { type RunFile } from './improve.js';
|
|
3
2
|
export declare const maxNoteLength = 4000;
|
|
4
3
|
export declare const maxCorrectedBytes: number;
|
|
5
4
|
export type Judgment = {
|
|
@@ -12,21 +11,17 @@ export type Judgment = {
|
|
|
12
11
|
};
|
|
13
12
|
/** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
|
|
14
13
|
export declare function correctedValue(path: string): unknown;
|
|
15
|
-
/** The line that says what was recorded. */
|
|
16
|
-
export declare function judgmentLine(judgment: Pick<Judgment, 'verdict' | 'step_id'>): string;
|
|
17
14
|
/**
|
|
18
15
|
* `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
|
|
19
16
|
* to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both. When the run's
|
|
20
17
|
* content stays on this computer (run_data: device), the account gets the verdict and step only, and the judgment with
|
|
21
|
-
* its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the same Method already runs on this computer, the correction joins it: that fix takes it in one more pass, and this
|
|
25
|
-
* command waits for it. It prints one line: what changed, or why nothing did.
|
|
18
|
+
* its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then starts
|
|
19
|
+
* the fix on this computer in its own process and returns at once with one line; method wait prints what the fix did,
|
|
20
|
+
* and so does the next method run of the Method.
|
|
26
21
|
*/
|
|
27
|
-
export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
23
|
+
/** The run's Method file fixes the correction: the file that ran the run, else the Method's home file on this computer. */
|
|
24
|
+
export declare function fileForJudgment(server: string, judgment: Pick<Judgment, 'method_id'>, runFile: string | undefined): string | undefined;
|
|
30
25
|
/** Two judgments of the same run and step by two people with opposite verdicts: neither is fixed until one is kept. */
|
|
31
26
|
export type HeldPair = {
|
|
32
27
|
run_id: string;
|
|
@@ -38,3 +33,9 @@ export type HeldPair = {
|
|
|
38
33
|
note: string | null;
|
|
39
34
|
}>;
|
|
40
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* `method answer JUDGMENT_ID "THEIR WORDS"`: send the person's answer to the question that a fix asked, and start the
|
|
38
|
+
* fix again with it. For a Method whose run content stays on this computer, the account keeps no answer, so the words
|
|
39
|
+
* also stay next to the Method file.
|
|
40
|
+
*/
|
|
41
|
+
export declare function answerJudgmentCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
@@ -3,8 +3,11 @@ import { basename } from 'node:path';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { DEFAULT_SERVER } from './method-client.js';
|
|
5
5
|
import { deviceRun, findRunFolder, methodFileOfRun, saveLocalJudgment } from './local-judgments.js';
|
|
6
|
-
import { fixCorrection, waitingLine } from './improve.js';
|
|
7
6
|
import { catchUp, readHome } from './home.js';
|
|
7
|
+
import { readDocument } from './authoring.js';
|
|
8
|
+
import { localJudgments } from './local-judgments.js';
|
|
9
|
+
import { fixStarter, markWorking, saveError, storedOutcomes } from './fix-outcomes.js';
|
|
10
|
+
import { approvedLine, heldPairLine, notHereLine, workingAnswerLine, workingLine } from './fix-lines.js';
|
|
8
11
|
export const maxNoteLength = 4000;
|
|
9
12
|
export const maxCorrectedBytes = 1024 * 1024;
|
|
10
13
|
/** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
|
|
@@ -26,30 +29,22 @@ export function correctedValue(path) {
|
|
|
26
29
|
return text;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
|
-
/** The line that says what was recorded. */
|
|
30
|
-
export function judgmentLine(judgment) {
|
|
31
|
-
return `Recorded: ${judgment.verdict}${judgment.step_id ? ` on step ${judgment.step_id}` : ''}.`;
|
|
32
|
-
}
|
|
33
32
|
/**
|
|
34
33
|
* `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
|
|
35
34
|
* to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both. When the run's
|
|
36
35
|
* content stays on this computer (run_data: device), the account gets the verdict and step only, and the judgment with
|
|
37
|
-
* its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* the same Method already runs on this computer, the correction joins it: that fix takes it in one more pass, and this
|
|
41
|
-
* command waits for it. It prints one line: what changed, or why nothing did.
|
|
36
|
+
* its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then starts
|
|
37
|
+
* the fix on this computer in its own process and returns at once with one line; method wait prints what the fix did,
|
|
38
|
+
* and so does the next method run of the Method.
|
|
42
39
|
*/
|
|
43
|
-
export async function judgeCommand(args, clientFactory
|
|
44
|
-
// The output stream at the start: a fix that runs in this process sends its run's own output to a log meanwhile.
|
|
45
|
-
const print = process.stdout.write.bind(process.stdout);
|
|
40
|
+
export async function judgeCommand(args, clientFactory) {
|
|
46
41
|
const usage = 'Use method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE], or method judge RUN_ID keep JUDGMENT_ID.';
|
|
47
42
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
48
43
|
step: { type: 'string' }, note: { type: 'string' }, corrected: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' }
|
|
49
44
|
} });
|
|
50
45
|
const [runId, verdict] = positionals;
|
|
51
46
|
if (runId && verdict === 'keep' && positionals.length === 3)
|
|
52
|
-
return keepCommand(runId, positionals[2], values, clientFactory
|
|
47
|
+
return keepCommand(runId, positionals[2], values, clientFactory);
|
|
53
48
|
if (!runId || !verdict || positionals.length !== 2)
|
|
54
49
|
throw Error(usage);
|
|
55
50
|
if (verdict !== 'right' && verdict !== 'wrong')
|
|
@@ -77,41 +72,45 @@ export async function judgeCommand(args, clientFactory, options = {}) {
|
|
|
77
72
|
id: response.judgment.id, run_id: runId, account_run_id: response.judgment.run_id ?? null, run_dir: runDir, method_id: response.judgment.method_id ?? null,
|
|
78
73
|
file: basename(runFile), step_id: step ?? null, verdict, note: note ?? null, ...(corrected !== undefined ? { corrected } : {}), created_at: response.judgment.created_at,
|
|
79
74
|
});
|
|
80
|
-
const recorded = judgmentLine(response.judgment);
|
|
81
75
|
// A judgment that contradicts another person's judgment of the same step is held: show both, with the two choices.
|
|
82
76
|
const pairs = response.held ? await heldPairs(client, response.judgment.method_id, response.judgment.id) : [];
|
|
83
77
|
if (verdict === 'right' || response.held) {
|
|
84
|
-
print(values.json ?
|
|
78
|
+
print(values.json ? { judgment: response.judgment, ...(pairs.length ? { held: pairs } : {}) } : [verdict === 'right' && !response.held ? approvedLine : 'Noted.', ...pairs.flatMap(heldLines)]);
|
|
85
79
|
return;
|
|
86
80
|
}
|
|
87
|
-
return
|
|
81
|
+
return startFix(client, response.judgment, runFile, values);
|
|
88
82
|
}
|
|
83
|
+
const print = (value) => process.stdout.write(Array.isArray(value) ? value.join('\n') + '\n' : JSON.stringify(value, null, 2) + '\n');
|
|
89
84
|
/** The run's Method file fixes the correction: the file that ran the run, else the Method's home file on this computer. */
|
|
90
|
-
|
|
91
|
-
const home = judgment.method_id ? readHome(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
export function fileForJudgment(server, judgment, runFile) {
|
|
86
|
+
const home = judgment.method_id ? readHome(server, judgment.method_id)?.path : undefined;
|
|
87
|
+
return runFile ?? (home && existsSync(home) ? home : undefined);
|
|
88
|
+
}
|
|
89
|
+
/** Start the fix of a correction in its own process and return: method wait gives its outcome. */
|
|
90
|
+
async function startFix(client, judgment, runFile, values, line = workingLine) {
|
|
91
|
+
const file = fileForJudgment(client.server, judgment, runFile);
|
|
92
|
+
if (!file || !judgment.method_id) {
|
|
93
|
+
print(values.json ? { judgment, fix: null } : [notHereLine]);
|
|
97
94
|
return;
|
|
98
95
|
}
|
|
99
96
|
// The fix starts from the newest version; a teammate's change of the same step stops it until the person combines them.
|
|
100
97
|
const caught = await catchUp(client, file, 'fixing');
|
|
101
98
|
if (caught.status === 'conflict') {
|
|
102
|
-
print(values.json ?
|
|
99
|
+
print(values.json ? { judgment, fix: null, stopped: caught.result } : [caught.result.failure.headline]);
|
|
103
100
|
process.exitCode = 2;
|
|
104
101
|
return;
|
|
105
102
|
}
|
|
106
103
|
if ('line' in caught)
|
|
107
104
|
process.stderr.write(caught.line + '\n');
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
markWorking(judgment.method_id, file, [judgment.id]);
|
|
106
|
+
try {
|
|
107
|
+
await fixStarter.start(file, judgment.method_id, [judgment.id], client.server);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
saveError(judgment.method_id, file, judgment.id, error.message);
|
|
111
|
+
throw error;
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
print([line, ...held].join('\n') + '\n');
|
|
113
|
+
print(values.json ? { judgment, fix: { status: 'working', wait: `method wait ${judgment.id}` } } : [line]);
|
|
115
114
|
}
|
|
116
115
|
/** The held pairs of a Method, from its judgments in the account. Nothing when they cannot be read. */
|
|
117
116
|
async function heldPairs(client, methodId, mine) {
|
|
@@ -136,20 +135,45 @@ async function heldPairs(client, methodId, mine) {
|
|
|
136
135
|
}
|
|
137
136
|
return pairs;
|
|
138
137
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
};
|
|
138
|
+
/** The person's line of a held pair, then the agent's line with the two commands. */
|
|
139
|
+
const heldLines = (pair) => [heldPairLine(pair.judgments),
|
|
140
|
+
`Ask the person which one is right, then keep it: ${pair.judgments.map(j => `method judge ${pair.run_id} keep ${j.id} (${j.noted_by ?? 'this one'}'s)`).join(' or ')}.`];
|
|
143
141
|
/** `method judge RUN keep JUDGMENT_ID`: keep this judgment of a held pair; the other one is set aside. A kept wrong judgment is fixed. */
|
|
144
|
-
async function keepCommand(runId, judgmentId, values, clientFactory
|
|
142
|
+
async function keepCommand(runId, judgmentId, values, clientFactory) {
|
|
145
143
|
const client = clientFactory(values.server ?? DEFAULT_SERVER);
|
|
146
144
|
const response = await client.request(`/api/judgments/${encodeURIComponent(judgmentId)}/settle`, 'POST', { keep: true });
|
|
147
145
|
const kept = response.judgment;
|
|
148
|
-
const recorded = `Kept ${kept?.noted_by ? `${kept.noted_by}'s
|
|
146
|
+
const recorded = `Kept ${kept?.noted_by ? `${kept.noted_by}'s view` : 'this view'}: the result is ${kept?.verdict ?? 'as judged'}.`;
|
|
149
147
|
if (kept?.verdict !== 'wrong' || !kept.method_id) {
|
|
150
|
-
|
|
148
|
+
print(values.json ? { judgment: kept ?? { id: judgmentId } } : [recorded]);
|
|
151
149
|
return;
|
|
152
150
|
}
|
|
153
151
|
const runDir = findRunFolder(runId);
|
|
154
|
-
return
|
|
152
|
+
return startFix(client, kept, runDir ? methodFileOfRun(runDir) : undefined, values, `${recorded} ${workingLine}`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* `method answer JUDGMENT_ID "THEIR WORDS"`: send the person's answer to the question that a fix asked, and start the
|
|
156
|
+
* fix again with it. For a Method whose run content stays on this computer, the account keeps no answer, so the words
|
|
157
|
+
* also stay next to the Method file.
|
|
158
|
+
*/
|
|
159
|
+
export async function answerJudgmentCommand(args, clientFactory) {
|
|
160
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { json: { type: 'boolean' }, server: { type: 'string' } } });
|
|
161
|
+
const [id, ...words] = positionals, answer = words.join(' ').trim();
|
|
162
|
+
if (!id || !answer)
|
|
163
|
+
throw Error('Use method answer JUDGMENT_ID "THE PERSON\'S WORDS".');
|
|
164
|
+
if (answer.length > maxNoteLength)
|
|
165
|
+
throw Error(`The answer has ${answer.length} characters; the most is ${maxNoteLength}.`);
|
|
166
|
+
const stored = storedOutcomes().find(r => r.judgment === id);
|
|
167
|
+
const file = stored && existsSync(stored.file) ? stored.file : undefined;
|
|
168
|
+
const device = !!file && readDocument(file).run_data === 'device';
|
|
169
|
+
const client = clientFactory(values.server ?? DEFAULT_SERVER);
|
|
170
|
+
// The account keeps no answer for a device Method; the words stay next to the Method file.
|
|
171
|
+
const response = await client.request(`/api/cli/judgments/${encodeURIComponent(id)}/answer`, 'POST', { answer });
|
|
172
|
+
const judgment = response.judgment ?? { id, method_id: stored?.method_id };
|
|
173
|
+
if (device && file) {
|
|
174
|
+
const before = localJudgments(file).filter(j => j.id === id).at(-1);
|
|
175
|
+
if (before)
|
|
176
|
+
saveLocalJudgment(file, { ...before, answer });
|
|
177
|
+
}
|
|
178
|
+
return startFix(client, { id, method_id: judgment.method_id ?? stored?.method_id ?? '' }, file, values, workingAnswerLine);
|
|
155
179
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type MethodClient } from './method-client.js';
|
|
2
|
+
export type LearnedRule = {
|
|
3
|
+
id: string;
|
|
4
|
+
when: string | null;
|
|
5
|
+
then: string;
|
|
6
|
+
by: string | null;
|
|
7
|
+
from: {
|
|
8
|
+
judgment?: string;
|
|
9
|
+
case?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
status: 'active' | 'retired';
|
|
12
|
+
created: string;
|
|
13
|
+
retired_at: string | null;
|
|
14
|
+
retired_reason: string | null;
|
|
15
|
+
/** What the fix loop keeps about a rule: base_failures, base_runs, dormant, merged_from. */
|
|
16
|
+
meta?: {
|
|
17
|
+
base_failures?: number;
|
|
18
|
+
base_runs?: number;
|
|
19
|
+
dormant?: boolean;
|
|
20
|
+
merged_from?: string[];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type Learned = {
|
|
24
|
+
format: 'method-learned/1';
|
|
25
|
+
steps: Record<string, LearnedRule[]>;
|
|
26
|
+
};
|
|
27
|
+
export declare const learnedFile: (file: string) => string;
|
|
28
|
+
export declare function readLearned(file: string): Learned;
|
|
29
|
+
/** The active rules of a Method in a stable order: by step, then by date. */
|
|
30
|
+
export declare function activeRules(learned: Learned): {
|
|
31
|
+
step: string;
|
|
32
|
+
rule: LearnedRule;
|
|
33
|
+
}[];
|
|
34
|
+
/** How often each rule applied, from the learned.applied events of this Method's runs on this computer. */
|
|
35
|
+
export declare function appliedCounts(file: string): Promise<Map<string, number>>;
|
|
36
|
+
/** A rule in plain words: "When the client is Acme, accept five years." */
|
|
37
|
+
export declare const ruleText: (rule: Pick<LearnedRule, "when" | "then">) => string;
|
|
38
|
+
/**
|
|
39
|
+
* `method learned FILE [--remove NUMBER|ID] [--json]`: the rules the agent learned from corrections, in plain words,
|
|
40
|
+
* with who taught each one and how often it applied. --remove retires one rule (by its number in the list or its ID);
|
|
41
|
+
* the change is saved as a new version, and method undo brings it back.
|
|
42
|
+
*/
|
|
43
|
+
export declare function learnedCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { authoringPath } from './authoring.js';
|
|
5
|
+
import { DEFAULT_SERVER } from './method-client.js';
|
|
6
|
+
import { recordChange, saveVersion, snapshot } from './undo.js';
|
|
7
|
+
import { removedRuleLine } from './fix-lines.js';
|
|
8
|
+
export const learnedFile = (file) => join(dirname(file), 'learned.json');
|
|
9
|
+
export function readLearned(file) {
|
|
10
|
+
const path = learnedFile(file);
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return { format: 'method-learned/1', steps: {} };
|
|
13
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
14
|
+
if (value?.format !== 'method-learned/1' || typeof value.steps !== 'object')
|
|
15
|
+
throw Error(`${basename(path)} is not a learned rules file.`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
/** The active rules of a Method in a stable order: by step, then by date. */
|
|
19
|
+
export function activeRules(learned) {
|
|
20
|
+
return Object.entries(learned.steps).flatMap(([step, rules]) => rules.filter(r => r.status === 'active').map(rule => ({ step, rule })))
|
|
21
|
+
.sort((a, b) => a.rule.created.localeCompare(b.rule.created));
|
|
22
|
+
}
|
|
23
|
+
/** How often each rule applied, from the learned.applied events of this Method's runs on this computer. */
|
|
24
|
+
export async function appliedCounts(file) {
|
|
25
|
+
const { localRuns } = await import('./proposals.js');
|
|
26
|
+
const counts = new Map();
|
|
27
|
+
for (const run of await localRuns(file)) {
|
|
28
|
+
let text = '';
|
|
29
|
+
try {
|
|
30
|
+
text = readFileSync(join(run.dir, 'events.jsonl'), 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const line of text.split('\n')) {
|
|
36
|
+
if (!line.includes('learned.applied'))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
const event = JSON.parse(line);
|
|
40
|
+
if (event.event === 'learned.applied')
|
|
41
|
+
for (const id of event.applied ?? [])
|
|
42
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
catch { /* a partial line */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return counts;
|
|
48
|
+
}
|
|
49
|
+
const day = (iso) => { const date = new Date(iso); return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }); };
|
|
50
|
+
/** A rule in plain words: "When the client is Acme, accept five years." */
|
|
51
|
+
export const ruleText = (rule) => {
|
|
52
|
+
const then = rule.then.trim().replace(/[.\s]+$/, '');
|
|
53
|
+
// A condition reads as part of the sentence: "The email is in German." becomes "When the email is in German, ...".
|
|
54
|
+
const when = rule.when?.trim().replace(/^when\s+/i, '').replace(/[.,\s]+$/, '').replace(/^[A-Z](?=[a-z])/, c => c.toLowerCase());
|
|
55
|
+
return when ? `When ${when}, ${then.charAt(0).toLowerCase()}${then.slice(1)}.` : `${then}.`;
|
|
56
|
+
};
|
|
57
|
+
const usedText = (n) => n === 0 ? 'not used yet' : n === 1 ? 'used once' : `used ${n} times`;
|
|
58
|
+
/**
|
|
59
|
+
* `method learned FILE [--remove NUMBER|ID] [--json]`: the rules the agent learned from corrections, in plain words,
|
|
60
|
+
* with who taught each one and how often it applied. --remove retires one rule (by its number in the list or its ID);
|
|
61
|
+
* the change is saved as a new version, and method undo brings it back.
|
|
62
|
+
*/
|
|
63
|
+
export async function learnedCommand(args, clientFactory) {
|
|
64
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { remove: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' } } });
|
|
65
|
+
if (positionals.length !== 1)
|
|
66
|
+
throw Error('Use method learned FILE [--remove NUMBER].');
|
|
67
|
+
const file = authoringPath(positionals[0]);
|
|
68
|
+
const learned = readLearned(file), rules = activeRules(learned);
|
|
69
|
+
if (values.remove !== undefined) {
|
|
70
|
+
const wanted = values.remove.trim(), index = /^\d+$/.test(wanted) ? Number(wanted) - 1 : rules.findIndex(r => r.rule.id === wanted);
|
|
71
|
+
const chosen = rules[index];
|
|
72
|
+
if (!chosen)
|
|
73
|
+
throw Error(`There is no learned rule ${wanted}. List them with method learned ${basename(file)}.`);
|
|
74
|
+
const client = clientFactory(values.server ?? DEFAULT_SERVER);
|
|
75
|
+
const { signedInName } = await import('./quality.js');
|
|
76
|
+
const name = await signedInName(client);
|
|
77
|
+
const before = snapshot(file, ['learned.json']);
|
|
78
|
+
Object.assign(chosen.rule, { status: 'retired', retired_at: new Date().toISOString(), retired_reason: `Removed by ${name ?? 'the person'}.` });
|
|
79
|
+
writeFileSync(learnedFile(file), JSON.stringify(learned, null, 2) + '\n');
|
|
80
|
+
const text = ruleText(chosen.rule);
|
|
81
|
+
recordChange(file, 'learned', `Removed the learned rule "${text}"`, `the rule "${text}" is back`, before);
|
|
82
|
+
await saveVersion(client, file, `Removed a learned rule: ${text}`.slice(0, 500));
|
|
83
|
+
process.stdout.write(values.json ? JSON.stringify({ removed: chosen.rule }, null, 2) + '\n' : removedRuleLine(text) + '\n');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const counts = await appliedCounts(file);
|
|
87
|
+
if (values.json) {
|
|
88
|
+
process.stdout.write(JSON.stringify({ rules: rules.map(({ step, rule }) => ({ ...rule, step, applied: counts.get(rule.id) ?? 0, text: ruleText(rule) })) }, null, 2) + '\n');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!rules.length) {
|
|
92
|
+
process.stdout.write('This agent has not learned any rules from corrections yet.\n');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const lines = rules.map(({ rule }, i) => `${i + 1}. ${ruleText(rule)} (${[rule.by, day(rule.created)].filter(Boolean).join(', ')}${rule.by || day(rule.created) ? '; ' : ''}${usedText(counts.get(rule.id) ?? 0)})`);
|
|
96
|
+
process.stdout.write(['What this agent learned from corrections:', ...lines, `Remove one: method learned ${basename(file)} --remove NUMBER`].join('\n') + '\n');
|
|
97
|
+
}
|
|
@@ -49,8 +49,9 @@ export async function run(target, flags, syncFactory) {
|
|
|
49
49
|
flags = await withInputsFrom(flags, flags["run-dir"], path, client);
|
|
50
50
|
}
|
|
51
51
|
const account = syncFactory ? undefined : await accountSync(path, flags);
|
|
52
|
+
let result;
|
|
52
53
|
try {
|
|
53
|
-
await runCurrentFile(path, flags, syncFactory ?? account?.sync);
|
|
54
|
+
result = await runCurrentFile(path, flags, syncFactory ?? account?.sync);
|
|
54
55
|
}
|
|
55
56
|
finally {
|
|
56
57
|
if (account) {
|
|
@@ -60,6 +61,12 @@ export async function run(target, flags, syncFactory) {
|
|
|
60
61
|
// Script cards are made after the run, so that this run's inputs can be replayed. They never delay or fail it.
|
|
61
62
|
if (savedVersions(account.client.server)[account.version.version_id])
|
|
62
63
|
(await import("./explain.js")).startBackgroundExplain(path, account.version.workflow, account.version.version_id, account.client.server);
|
|
64
|
+
// The output of this run is judged against the corrections that recent fixes answered, in the background.
|
|
65
|
+
if (result?.status === "completed" && !flags.resume) {
|
|
66
|
+
const { watchedCorrections } = await import("./watch.js");
|
|
67
|
+
if (watchedCorrections(path).length)
|
|
68
|
+
(await import("./fix-outcomes.js")).fixStarter.watch(path, flags["run-dir"], account.client.server);
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
}
|
|
@@ -15,6 +15,8 @@ export type LocalJudgment = {
|
|
|
15
15
|
note: string | null;
|
|
16
16
|
corrected?: unknown;
|
|
17
17
|
created_at: string;
|
|
18
|
+
/** The person's answer to the fix's question; a later line of the same judgment adds it. */
|
|
19
|
+
answer?: string;
|
|
18
20
|
};
|
|
19
21
|
/** The file that a run folder records: the Method file that method run ran. */
|
|
20
22
|
export declare const methodFileRecord = "method-file.json";
|
|
@@ -3,12 +3,13 @@ import type { MethodClient } from './method-client.js';
|
|
|
3
3
|
export declare const runtimeVersion: string;
|
|
4
4
|
export declare const fileHash: (data: Uint8Array) => string;
|
|
5
5
|
export declare function contained(root: string, name: string): string;
|
|
6
|
-
/** The files that a saved package holds: declared files, script entrypoints, and
|
|
6
|
+
/** The files that a saved package holds: declared files, script entrypoints, dependency lockfiles, and learned rules. */
|
|
7
7
|
export declare function packageFileNames(root: string, workflow: any): string[];
|
|
8
8
|
/**
|
|
9
9
|
* The files of the Method's cases/ folder. A Method with account run data saves its cases with each version; a Method
|
|
10
10
|
* with device run data keeps them on this computer. Hidden files and links are not included. A file over the package
|
|
11
|
-
* limit of 20 MB is not included; its name goes to tooLarge, so the save can say so.
|
|
11
|
+
* limit of 20 MB is not included; its name goes to tooLarge, so the save can say so. A case's snapshot/ folder (copies of
|
|
12
|
+
* the files its run read) is never included: it stays on this computer.
|
|
12
13
|
*/
|
|
13
14
|
export declare const maxPackageFileBytes = 20000000;
|
|
14
15
|
export declare function caseFileNames(root: string, tooLarge?: string[]): string[];
|
|
@@ -13,17 +13,18 @@ export function contained(root, name) {
|
|
|
13
13
|
throw Error(`File leaves the Method folder: ${name}`);
|
|
14
14
|
return path;
|
|
15
15
|
}
|
|
16
|
-
/** The files that a saved package holds: declared files, script entrypoints, and
|
|
16
|
+
/** The files that a saved package holds: declared files, script entrypoints, dependency lockfiles, and learned rules. */
|
|
17
17
|
export function packageFileNames(root, workflow) {
|
|
18
18
|
const executions = [...Object.values(workflow.steps).flatMap((s) => [s.do, s.check, ...Object.values(s.effects ?? {}).flatMap((e) => [e.observe, e.judge])]),
|
|
19
19
|
...Object.values(workflow.tools ?? {}).map((t) => t.run)].filter(Boolean);
|
|
20
20
|
return [...new Set([...(workflow.files ?? []), ...executions.filter(e => e.kind === 'run').map(e => e.entrypoint),
|
|
21
|
-
...['package.json', 'package-lock.json', 'pyproject.toml', 'uv.lock'].filter(name => existsSync(resolve(root, name)))])].sort();
|
|
21
|
+
...['package.json', 'package-lock.json', 'pyproject.toml', 'uv.lock', 'learned.json'].filter(name => existsSync(resolve(root, name)))])].sort();
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* The files of the Method's cases/ folder. A Method with account run data saves its cases with each version; a Method
|
|
25
25
|
* with device run data keeps them on this computer. Hidden files and links are not included. A file over the package
|
|
26
|
-
* limit of 20 MB is not included; its name goes to tooLarge, so the save can say so.
|
|
26
|
+
* limit of 20 MB is not included; its name goes to tooLarge, so the save can say so. A case's snapshot/ folder (copies of
|
|
27
|
+
* the files its run read) is never included: it stays on this computer.
|
|
27
28
|
*/
|
|
28
29
|
export const maxPackageFileBytes = 20_000_000;
|
|
29
30
|
export function caseFileNames(root, tooLarge = []) {
|
|
@@ -36,6 +37,9 @@ export function caseFileNames(root, tooLarge = []) {
|
|
|
36
37
|
const name = `${relativeDir}/${entry.name}`;
|
|
37
38
|
if (entry.name.startsWith('.'))
|
|
38
39
|
continue;
|
|
40
|
+
// cases/ID/snapshot/ holds copies of what a run read (client documents): it stays on this computer.
|
|
41
|
+
if (entry.isDirectory() && entry.name === 'snapshot' && /^cases\/[^/]+$/.test(relativeDir))
|
|
42
|
+
continue;
|
|
39
43
|
try {
|
|
40
44
|
packagePath(name);
|
|
41
45
|
}
|
|
@@ -12,7 +12,7 @@ export declare const commandHelp: Record<string, Command>;
|
|
|
12
12
|
export declare function renderCommand(name: string, includeCommon?: boolean): string;
|
|
13
13
|
export declare const guideTopics: readonly ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
|
|
14
14
|
export declare function authoringGuide(topic?: string, exampleId?: string): string;
|
|
15
|
-
export declare const overviewHelp = "Method \u2014 author, save and run methods with your coding agent.\n\nBefore creating, editing, or proposing a Method, run `method authoring` and read its guidance. Do this before choosing an existing Method as a reference.\n\nLearn: method authoring Start the installed guide.\n method authoring all Read the full manual offline.\n method COMMAND --help Read arguments, results and examples.\n method help step add Read help for a subcommand.\n method schema [TYPE] Read a formal JSON schema.\n\nLocal drafts: init, show, set, remove, step add/update/remove/move,\n check set/remove, validate, diff.\nOnline documents: list, get, steps, step, publish, explain.\nResults and improvements: judge, changes.\nKeys for scripts: secret find, secret import, secret set, secret list.\nExecution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.\nEffects and corrections: effect add/list, case new/retire/list, test.\nInputs and shared state: bind, state.\nBrowser and computer setup: browser connect, config.\nAccount access: status, login, logout, devices, revoke.\nProduction runs: connect, answer, keys create/list/revoke, worker.\n\nSign in with method login first. Read the authoring guide, give each prompt its own step, write the files, validate, and run. Each signed-in run saves a version when the file changed. Publish a version when the user wants to share or schedule it.\nUse method run task.method to run it.\nOnline server: https://app.withmethod.ai. Use --server only to select another\nserver. Sign-in uses browser approval. Never send credentials in chat.\n";
|
|
15
|
+
export declare const overviewHelp = "Method \u2014 author, save and run methods with your coding agent.\n\nBefore creating, editing, or proposing a Method, run `method authoring` and read its guidance. Do this before choosing an existing Method as a reference.\n\nLearn: method authoring Start the installed guide.\n method authoring all Read the full manual offline.\n method COMMAND --help Read arguments, results and examples.\n method help step add Read help for a subcommand.\n method schema [TYPE] Read a formal JSON schema.\n\nLocal drafts: init, show, set, remove, step add/update/remove/move,\n check set/remove, validate, diff.\nOnline documents: list, get, steps, step, publish, explain.\nResults and improvements: judge, wait, answer, learned, undo, changes.\nKeys for scripts: secret find, secret import, secret set, secret list.\nExecution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.\nEffects and corrections: effect add/list, case new/retire/list, test.\nInputs and shared state: bind, state.\nBrowser and computer setup: browser connect, config.\nAccount access: status, login, logout, devices, revoke.\nProduction runs: connect, answer, keys create/list/revoke, worker.\n\nSign in with method login first. Read the authoring guide, give each prompt its own step, write the files, validate, and run. Each signed-in run saves a version when the file changed. Publish a version when the user wants to share or schedule it.\nUse method run task.method to run it.\nOnline server: https://app.withmethod.ai. Use --server only to select another\nserver. Sign-in uses browser approval. Never send credentials in chat.\n";
|
|
16
16
|
/** Resolve help before parsing flags, creating a client, or touching any draft. */
|
|
17
17
|
export declare function methodHelp(args: string[]): string | undefined;
|
|
18
18
|
/** The CLI and published reference use this same command inventory. */
|