@method-ai-hq/sdk 0.19.0 → 0.20.1
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 +132 -34
- package/dist/packages/sdk/src/improve.js +523 -176
- 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
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
MIT-licensed JavaScript API and the `method` CLI. Source and releases: https://github.com/method-ai-hq/method-sdk
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.
|
|
6
|
+
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.20.1/method-ai-hq-sdk-0.20.1.tgz
|
|
7
7
|
npx method --version
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@method-ai-hq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
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/v0.
|
|
38
|
+
"@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/v0.16.2",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"yaml": "^2.9.0",
|
|
41
41
|
"zod": "^3.25.76"
|
|
@@ -16,5 +16,5 @@ export declare const costRules = "Before describing execution cost, check how th
|
|
|
16
16
|
/** What to watch after a Method works. */
|
|
17
17
|
export declare const watchRules = "# Watch it\n\n- Look at failed and unconfirmed runs on the dashboard. A run that needs attention says which step failed and why.\n- For classify, watch how often the choice is `unclear` or below your threshold. A rise means the inputs changed.\n- For slow effects such as email delivery, schedule `method observe --pending`.\n";
|
|
18
18
|
/** What to do after a Method works: production, improvement, and script cards. */
|
|
19
|
-
export declare const afterRules = "# After it works\n\n- **Put it in an app:** `method connect APP_FOLDER` publishes it and prints the code to add.\n- **Record each reaction:** after you show a result, run `method judge RUN_ID wrong --note \"THEIR WORDS\"` (with `--corrected FILE` when they gave the right result) when the user corrects it or asks to change the Method because of it, and `method judge RUN_ID right` when they use or approve it. Do not ask them to confirm it, and do not edit the Method yourself for the same correction
|
|
19
|
+
export declare const afterRules = "# After it works\n\n- **Put it in an app:** `method connect APP_FOLDER` publishes it and prints the code to add.\n- **Record each reaction:** after you show a result, run `method judge RUN_ID wrong --note \"THEIR WORDS\"` (with `--corrected FILE` when they gave the right result) when the user corrects it or asks to change the Method because of it, and `method judge RUN_ID right` when they use or approve it. Do not ask them to confirm it, and do not edit the Method yourself for the same correction. A wrong judgment returns at once and tests a fix in the background with the user's own Claude Code or Codex. Run `method wait` for the result and tell the user its line. After a fix, show the tested output that `method wait` prints; never run a Method that sends or changes things again just to show it. When it prints a question, show it to the user word for word, never answer it yourself, and send their words with `method answer JUDGMENT_ID \"THEIR WORDS\"`. `method learned FILE` lists what the agent learned from corrections; `method undo` restores the Method as it was before Method's last change.\n- **Explain the scripts:** `method explain FILE` writes a card for each script step.\n";
|
|
20
20
|
export declare const exampleSelection = "After choosing the step types, read the examples that fit the design. Use their syntax; choose the steps for this task yourself.";
|
|
@@ -152,7 +152,7 @@ export const watchRules = `# Watch it
|
|
|
152
152
|
export const afterRules = `# After it works
|
|
153
153
|
|
|
154
154
|
- **Put it in an app:** \`method connect APP_FOLDER\` publishes it and prints the code to add.
|
|
155
|
-
- **Record each reaction:** after you show a result, run \`method judge RUN_ID wrong --note "THEIR WORDS"\` (with \`--corrected FILE\` when they gave the right result) when the user corrects it or asks to change the Method because of it, and \`method judge RUN_ID right\` when they use or approve it. Do not ask them to confirm it, and do not edit the Method yourself for the same correction
|
|
155
|
+
- **Record each reaction:** after you show a result, run \`method judge RUN_ID wrong --note "THEIR WORDS"\` (with \`--corrected FILE\` when they gave the right result) when the user corrects it or asks to change the Method because of it, and \`method judge RUN_ID right\` when they use or approve it. Do not ask them to confirm it, and do not edit the Method yourself for the same correction. A wrong judgment returns at once and tests a fix in the background with the user's own Claude Code or Codex. Run \`method wait\` for the result and tell the user its line. After a fix, show the tested output that \`method wait\` prints; never run a Method that sends or changes things again just to show it. When it prints a question, show it to the user word for word, never answer it yourself, and send their words with \`method answer JUDGMENT_ID "THEIR WORDS"\`. \`method learned FILE\` lists what the agent learned from corrections; \`method undo\` restores the Method as it was before Method's last change.
|
|
156
156
|
- **Explain the scripts:** \`method explain FILE\` writes a card for each script step.
|
|
157
157
|
`;
|
|
158
158
|
export const exampleSelection = "After choosing the step types, read the examples that fit the design. Use their syntax; choose the steps for this task yourself.";
|
|
@@ -33,6 +33,7 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
|
|
|
33
33
|
const start = events.find(e => e.event === "run.started");
|
|
34
34
|
const invocations = {};
|
|
35
35
|
const setup = existsSync(join(root, 'setup.json')) ? read('setup.json') : [];
|
|
36
|
+
const ids = (value) => (Array.isArray(value) ? value : []).filter((id) => typeof id === 'string').slice(0, 500).map(id => id.slice(0, 100));
|
|
36
37
|
const runEvents = setup;
|
|
37
38
|
for (const event of events) {
|
|
38
39
|
// Keep useful trace fields; do not upload tool arguments, environment values, or process stdout.
|
|
@@ -58,6 +59,8 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
|
|
|
58
59
|
...(Number.isSafeInteger(event.total) ? { total: event.total, ...(typeof event.unit === "string" ? { unit: event.unit } : {}) } : {}),
|
|
59
60
|
...(typeof event.key === "string" ? { key: event.key } : {}),
|
|
60
61
|
...(Number.isSafeInteger(event.attempt) ? { attempt: event.attempt } : {}), ...(Number.isSafeInteger(event.attempts) ? { attempts: event.attempts } : {}),
|
|
62
|
+
// Rule IDs only: the server counts how often each learned rule applied.
|
|
63
|
+
...(event.event === "learned.applied" ? { considered: ids(event.considered), applied: ids(event.applied) } : {}),
|
|
61
64
|
...(event.entrypoint ? { command: [event.runtime, event.entrypoint, ...(event.args ?? [])].filter(Boolean).join(" ") } : typeof event.command === "string" ? { command: event.command } : {}),
|
|
62
65
|
...(event.exit_code !== undefined ? { exit_code: event.exit_code } : {}),
|
|
63
66
|
...(typeof event.diagnostics === "string" && event.diagnostics ? { diagnostics: event.diagnostics.length <= 4000 ? event.diagnostics : event.diagnostics.slice(0, 1000) + "\n[middle omitted]\n" + event.diagnostics.slice(-3000) } : {}),
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** What the fix loop did with one correction. */
|
|
2
|
+
export type OutcomeKind = 'fixed' | 'no_fix_found' | 'could_not_test' | 'question' | 'held' | 'not_reproduced' | 'not_fixable_here' | 'already_changed' | 'retry' | 'elsewhere';
|
|
3
|
+
export type CouldNotTestReason = 'missing_file' | 'can_send' | 'too_large' | 'no_live_step';
|
|
4
|
+
/** The output that a tested fix gave, so the agent can show it without running the Method again. */
|
|
5
|
+
export type TestedOutput = {
|
|
6
|
+
result?: unknown;
|
|
7
|
+
files?: Record<string, string>;
|
|
8
|
+
held_back?: Array<{
|
|
9
|
+
tool: string;
|
|
10
|
+
args: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export type CorrectionOutcome = {
|
|
14
|
+
id: string;
|
|
15
|
+
outcome: OutcomeKind;
|
|
16
|
+
/** fixed: what is different now, in the person's words ("replies now include the redline"). */
|
|
17
|
+
summary?: string;
|
|
18
|
+
/** could_not_test: why. */
|
|
19
|
+
reason?: CouldNotTestReason;
|
|
20
|
+
/** question: one question in the person's terms. */
|
|
21
|
+
question?: string;
|
|
22
|
+
/** no_fix_found: how many changes were tried. */
|
|
23
|
+
tried?: number;
|
|
24
|
+
/** fixed: how often the fix passed on this result, and on how many earlier results it was checked. */
|
|
25
|
+
tested?: {
|
|
26
|
+
times: number;
|
|
27
|
+
earlier: number;
|
|
28
|
+
};
|
|
29
|
+
/** The thing the Method works on, as a plain noun ("NDA", "reply"); the lines say "result" without it. */
|
|
30
|
+
subject?: string;
|
|
31
|
+
/** could_not_test can_send: what the agent can send ("email"). */
|
|
32
|
+
sends?: string;
|
|
33
|
+
/** could_not_test: the note was added as a learned rule that later runs check. */
|
|
34
|
+
rule_added?: boolean;
|
|
35
|
+
/** held and already_changed: the other person; held: what they asked for. */
|
|
36
|
+
by?: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
/** missing_file: the file that is gone. For --json only; never in a line. */
|
|
39
|
+
path?: string;
|
|
40
|
+
tested_output?: TestedOutput;
|
|
41
|
+
/** retry: another correction's fix changed the same part, so this one is fixed again on the new file in the next pass. */
|
|
42
|
+
again?: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** The line after method judge: the fix runs in the background. */
|
|
45
|
+
export declare const workingLine = "Working on it. I'll test a fix on this run.";
|
|
46
|
+
export declare const workingAnswerLine = "Thanks. Working on it with your answer. I'll test a fix on this run.";
|
|
47
|
+
export declare const waitingLine: (n: number) => string;
|
|
48
|
+
export declare const stillWorkingLine = "Still working on it. Run method wait again.";
|
|
49
|
+
export declare const nothingWaitingLine = "Nothing is being fixed on this computer.";
|
|
50
|
+
export declare const notHereLine = "Noted. The agent is not on this computer, so the next time it runs on a computer I'll test a fix there.";
|
|
51
|
+
export declare const approvedLine = "Noted: this result is right.";
|
|
52
|
+
export declare const stoppedLine = "The fix stopped before it finished. I'll try again on the next run.";
|
|
53
|
+
export declare const errorLine: (message: string) => string;
|
|
54
|
+
export declare const undoneLine: (what?: string) => string;
|
|
55
|
+
export declare const removedRuleLine: (rule: string) => string;
|
|
56
|
+
/** The line after a pruning change: what the sweep says it did (improve.method's summary), and how to undo it. */
|
|
57
|
+
export declare const prunedLine: (summary: string) => string;
|
|
58
|
+
/** A fix that broke an earlier result in the later check is undone, and its correction waits for a new fix. */
|
|
59
|
+
export declare const fixUndoneLine: (note: string | undefined) => string;
|
|
60
|
+
/** A rule that a pruning change removed came back because its problem came back. */
|
|
61
|
+
export declare const ruleBackLine: (rule: string) => string;
|
|
62
|
+
export declare const cameBackLine: (note: string | undefined) => string;
|
|
63
|
+
export declare const testedOutputHeading = "What the fixed agent gives now (tested; nothing was sent):";
|
|
64
|
+
export declare const heldBackHeading = "What it would send (held back during the test):";
|
|
65
|
+
/** The one line for what the loop did with a correction. */
|
|
66
|
+
export declare function outcomeLine(o: CorrectionOutcome): string;
|
|
67
|
+
/** The line for the agent after a question: it shows the question and sends the answer. Not for the person. */
|
|
68
|
+
export declare const answerNext: (judgmentId: string) => string;
|
|
69
|
+
/** Two people judged the same result in opposite ways. */
|
|
70
|
+
export type HeldPerson = {
|
|
71
|
+
verdict: string;
|
|
72
|
+
noted_by: string | null;
|
|
73
|
+
note: string | null;
|
|
74
|
+
};
|
|
75
|
+
export declare function heldPairLine(people: HeldPerson[]): string;
|
|
76
|
+
/** A correction as a person reads it: "Gráinne's correction ("the note")", or "A correction ("the note")". */
|
|
77
|
+
export declare const correctionName: (c: {
|
|
78
|
+
author?: string | null;
|
|
79
|
+
note?: unknown;
|
|
80
|
+
}) => string;
|
|
81
|
+
/** The run notice when the Method changed since its earlier corrections were checked. */
|
|
82
|
+
export declare const uncheckedLine: (count: number, named: string[], command: string) => string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Every line that a fix shows to the person. The words are about the person's work: no Method internals (no case,
|
|
2
|
+
// step, criterion, judge, version, IDs, paths or run counts). testkit/sdk/fix-lines.test.ts scans these lines.
|
|
3
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
4
|
+
const thing = (o) => o.subject?.trim() ? `this ${o.subject.trim()}` : 'this result';
|
|
5
|
+
const sentence = (text) => text.trim().replace(/[.\s]+$/, '');
|
|
6
|
+
const kept = (o) => o.rule_added ? 'I added your note as a rule and will check it on the next runs.' : "I'll keep your note.";
|
|
7
|
+
const quoted = (text) => text?.trim() ? ` ("${sentence(text).slice(0, 200)}")` : '';
|
|
8
|
+
/** The line after method judge: the fix runs in the background. */
|
|
9
|
+
export const workingLine = 'Working on it. I\'ll test a fix on this run.';
|
|
10
|
+
export const workingAnswerLine = 'Thanks. Working on it with your answer. I\'ll test a fix on this run.';
|
|
11
|
+
export const waitingLine = (n) => n === 1 ? 'Working on a correction that was waiting. I\'ll test a fix for it.' : `Working on ${n} corrections that were waiting. I'll test a fix for each.`;
|
|
12
|
+
export const stillWorkingLine = 'Still working on it. Run method wait again.';
|
|
13
|
+
export const nothingWaitingLine = 'Nothing is being fixed on this computer.';
|
|
14
|
+
export const notHereLine = 'Noted. The agent is not on this computer, so the next time it runs on a computer I\'ll test a fix there.';
|
|
15
|
+
export const approvedLine = 'Noted: this result is right.';
|
|
16
|
+
export const stoppedLine = 'The fix stopped before it finished. I\'ll try again on the next run.';
|
|
17
|
+
export const errorLine = (message) => `The fix stopped: ${sentence(message)}. I'll try again on the next run.`;
|
|
18
|
+
export const undoneLine = (what) => what?.trim() ? `Undone: ${sentence(what)}. The agent works as it did before.` : 'Undone. The agent works as it did before the last change.';
|
|
19
|
+
export const removedRuleLine = (rule) => `Removed: "${sentence(rule)}". Undo: method undo.`;
|
|
20
|
+
/** The line after a pruning change: what the sweep says it did (improve.method's summary), and how to undo it. */
|
|
21
|
+
export const prunedLine = (summary) => `${sentence(summary)}. Undo: method undo.`;
|
|
22
|
+
/** A fix that broke an earlier result in the later check is undone, and its correction waits for a new fix. */
|
|
23
|
+
export const fixUndoneLine = (note) => `I undid the fix for your correction${quoted(note)}: it changed a result that was right before. I'll try a new fix on the next run.`;
|
|
24
|
+
/** A rule that a pruning change removed came back because its problem came back. */
|
|
25
|
+
export const ruleBackLine = (rule) => `I put back the rule "${sentence(rule)}": without it, a problem you corrected came back.`;
|
|
26
|
+
export const cameBackLine = (note) => `A problem you corrected came back twice${quoted(note)}. I'll test a new fix. To go back to how it worked before the last change: method undo.`;
|
|
27
|
+
export const testedOutputHeading = 'What the fixed agent gives now (tested; nothing was sent):';
|
|
28
|
+
export const heldBackHeading = 'What it would send (held back during the test):';
|
|
29
|
+
/** The one line for what the loop did with a correction. */
|
|
30
|
+
export function outcomeLine(o) {
|
|
31
|
+
switch (o.outcome) {
|
|
32
|
+
case 'fixed': {
|
|
33
|
+
const tested = o.tested ? ` Tested ${plural(o.tested.times, 'time')} on ${thing(o)}${o.tested.earlier ? ` and on ${plural(o.tested.earlier, 'earlier one')}` : ''}.` : '';
|
|
34
|
+
return `Fixed${o.summary?.trim() ? `: ${sentence(o.summary)}` : ''}.${tested} Undo: method undo.`;
|
|
35
|
+
}
|
|
36
|
+
case 'no_fix_found':
|
|
37
|
+
return `No fix found yet: I tried ${o.tried ? plural(o.tried, 'change') : 'several changes'}; none fixed ${thing(o)} reliably. ${kept(o)}`;
|
|
38
|
+
case 'could_not_test': {
|
|
39
|
+
const why = o.reason === 'can_send' ? `this agent can send ${o.sends?.trim() || 'messages'}`
|
|
40
|
+
: o.reason === 'missing_file' ? `a file that ${thing(o)} came from is no longer on this computer`
|
|
41
|
+
: o.reason === 'too_large' ? `${thing(o)} came from more files than I can copy to test with`
|
|
42
|
+
: 'my change did not reach the part of the agent that made this result';
|
|
43
|
+
return `Could not test a fix safely: ${why}. ${kept(o)}`;
|
|
44
|
+
}
|
|
45
|
+
case 'question': return `I have one question before I change anything: ${o.question?.trim() ?? ''}`;
|
|
46
|
+
case 'held': return `Not changed: ${o.by ?? 'someone else'} wants a different result here${quoted(o.detail)}. Decide with them which one is right.`;
|
|
47
|
+
case 'not_reproduced': return `This mistake happens only sometimes, and I could not make it happen again. ${kept(o)} Tell me if you see it again.`;
|
|
48
|
+
case 'not_fixable_here': return 'This does not come from how the agent works: it likely comes from the input, the data, or a tool it uses. I\'ll keep your note.';
|
|
49
|
+
case 'already_changed': return `Not changed: ${o.by ? `${o.by} changed` : 'someone changed'} this part of the agent after this result, which may already fix it.`;
|
|
50
|
+
case 'retry': return 'Not changed yet: the agent changed while I tested a fix. I\'ll try again on the next run.';
|
|
51
|
+
case 'elsewhere': return 'Another computer is testing a fix for this.';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The line for the agent after a question: it shows the question and sends the answer. Not for the person. */
|
|
55
|
+
export const answerNext = (judgmentId) => `Show this question to the person word for word, and do not answer it yourself. Send their answer with: method answer ${judgmentId} "THEIR WORDS"`;
|
|
56
|
+
export function heldPairLine(people) {
|
|
57
|
+
const said = (j) => `${j.noted_by ?? 'Someone'} says it is ${j.verdict}${quoted(j.note ?? undefined)}`;
|
|
58
|
+
return `Two people disagree about this result: ${people.map(said).join('; ')}. Nothing changes until one is kept.`;
|
|
59
|
+
}
|
|
60
|
+
/** A correction as a person reads it: "Gráinne's correction ("the note")", or "A correction ("the note")". */
|
|
61
|
+
export const correctionName = (c) => `${c.author ? `${c.author}'s correction` : 'A correction'}${quoted(String(c.note ?? ''))}`;
|
|
62
|
+
/** The run notice when the Method changed since its earlier corrections were checked. */
|
|
63
|
+
export const uncheckedLine = (count, named, command) => `${plural(count, 'earlier correction')} ${count === 1 ? 'was' : 'were'} not checked since this agent changed${named.length ? ` (${named.join('; ')})` : ''}. Check ${count === 1 ? "it" : "them"}: ${command}`;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type CorrectionOutcome } from './fix-lines.js';
|
|
2
|
+
export type StoredOutcome = {
|
|
3
|
+
judgment: string;
|
|
4
|
+
method_id: string;
|
|
5
|
+
file: string;
|
|
6
|
+
status: 'working' | 'done' | 'stopped';
|
|
7
|
+
started_at: string;
|
|
8
|
+
pid?: number | undefined;
|
|
9
|
+
finished_at?: string;
|
|
10
|
+
outcome?: CorrectionOutcome;
|
|
11
|
+
error?: string;
|
|
12
|
+
log?: string;
|
|
13
|
+
shown: boolean;
|
|
14
|
+
/** A record that is not about one correction: a pruning change, or a fix that stopped holding. */
|
|
15
|
+
kind?: 'prune' | 'came_back';
|
|
16
|
+
line?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const fixFolder: (methodId: string) => string;
|
|
19
|
+
/** Mark corrections as being fixed now: method wait waits for them. */
|
|
20
|
+
export declare function markWorking(methodId: string, file: string, ids: string[], pid?: number): void;
|
|
21
|
+
/** The fix process that works on these corrections. */
|
|
22
|
+
export declare function setWorker(methodId: string, ids: string[], pid: number): void;
|
|
23
|
+
export declare function saveOutcome(methodId: string, file: string, outcome: CorrectionOutcome, log?: string): void;
|
|
24
|
+
export declare function saveError(methodId: string, file: string, id: string, error: string, log?: string): void;
|
|
25
|
+
/** A line that is not about one correction (a pruning change; a fix that came back). */
|
|
26
|
+
export declare function saveNotice(methodId: string, file: string, kind: 'prune' | 'came_back', line: string): void;
|
|
27
|
+
export declare function storedOutcome(methodId: string, id: string): StoredOutcome | undefined;
|
|
28
|
+
/** Every stored record, of one Method or of all; a working record whose process stopped reads as stopped. */
|
|
29
|
+
export declare function storedOutcomes(methodId?: string): StoredOutcome[];
|
|
30
|
+
export declare function markShown(record: StoredOutcome): void;
|
|
31
|
+
/** The lines of a record: the person's line, the tested output after a fix, and for a question the agent's next step. */
|
|
32
|
+
export declare function recordLines(record: StoredOutcome, options?: {
|
|
33
|
+
brief?: boolean;
|
|
34
|
+
}): string[];
|
|
35
|
+
/** The JSON of a record for --json: the outcome as the agent reads it, with the person's line. */
|
|
36
|
+
export declare const recordJson: (record: StoredOutcome) => {
|
|
37
|
+
next?: string;
|
|
38
|
+
lines: string[];
|
|
39
|
+
kind?: "prune" | "came_back";
|
|
40
|
+
error?: string;
|
|
41
|
+
outcome?: CorrectionOutcome;
|
|
42
|
+
judgment: string;
|
|
43
|
+
status: "done" | "stopped" | "working";
|
|
44
|
+
};
|
|
45
|
+
/** The lines of the outcomes of a Method that nobody has seen yet (without the tested output), marked seen. */
|
|
46
|
+
export declare function unshownLines(methodId: string): string[];
|
|
47
|
+
/** Starts the fix of these corrections in its own process; tests replace it. */
|
|
48
|
+
export declare const fixStarter: {
|
|
49
|
+
start(file: string, methodId: string, ids: string[], server: string, options?: {
|
|
50
|
+
prune?: boolean;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/** Judges the run's output against the corrections that recent fixes answered, in its own process. */
|
|
53
|
+
watch(file: string, runDir: string, server: string): void;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* `method wait [JUDGMENT_ID] [--timeout SECONDS] [--json]`: wait for the fix of a correction (or of every correction
|
|
57
|
+
* that is being fixed on this computer) and print what it did. A wait that ends before the fix says so in one line;
|
|
58
|
+
* the fix goes on, and method wait can run again.
|
|
59
|
+
*/
|
|
60
|
+
export declare function waitCommand(args: string[], options?: {
|
|
61
|
+
pollMs?: number;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
export declare function recordLastChange(file: string): void;
|
|
64
|
+
export declare function lastChangedFile(): string | undefined;
|
|
65
|
+
/** Fix state of a Method: fixes applied since the last pruning pass, and the agent that ran the last fix. */
|
|
66
|
+
export type FixState = {
|
|
67
|
+
fixes_since_prune: number;
|
|
68
|
+
agent: string | null;
|
|
69
|
+
};
|
|
70
|
+
export declare function readFixState(methodId: string): FixState;
|
|
71
|
+
export declare function writeFixState(methodId: string, state: FixState): void;
|
|
72
|
+
export declare function forgetOutcome(methodId: string, id: string): void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { methodCache } from './prepare.js';
|
|
6
|
+
import { writePrivateJson } from './files.js';
|
|
7
|
+
import { valueText } from '@withmethod/runtime/rubric.js';
|
|
8
|
+
import { answerNext, errorLine, heldBackHeading, outcomeLine, stoppedLine, testedOutputHeading, workingLine } from './fix-lines.js';
|
|
9
|
+
export const fixFolder = (methodId) => join(methodCache(), 'fixes', methodId);
|
|
10
|
+
const outcomesDir = (methodId) => join(fixFolder(methodId), 'outcomes');
|
|
11
|
+
const recordPath = (methodId, id) => join(outcomesDir(methodId), `${id}.json`);
|
|
12
|
+
const read = (path) => { try {
|
|
13
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
} };
|
|
18
|
+
function write(record) {
|
|
19
|
+
mkdirSync(outcomesDir(record.method_id), { recursive: true, mode: 0o700 });
|
|
20
|
+
const path = recordPath(record.method_id, record.judgment), temporary = `${path}.${process.pid}.tmp`;
|
|
21
|
+
writePrivateJson(temporary, record);
|
|
22
|
+
renameSync(temporary, path);
|
|
23
|
+
}
|
|
24
|
+
/** Mark corrections as being fixed now: method wait waits for them. */
|
|
25
|
+
export function markWorking(methodId, file, ids, pid) {
|
|
26
|
+
for (const id of ids)
|
|
27
|
+
write({ judgment: id, method_id: methodId, file: resolve(file), status: 'working', started_at: new Date().toISOString(), ...(pid ? { pid } : {}), shown: false });
|
|
28
|
+
}
|
|
29
|
+
/** The fix process that works on these corrections. */
|
|
30
|
+
export function setWorker(methodId, ids, pid) {
|
|
31
|
+
for (const id of ids) {
|
|
32
|
+
const record = read(recordPath(methodId, id));
|
|
33
|
+
if (record?.status === 'working')
|
|
34
|
+
write({ ...record, pid });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function saveOutcome(methodId, file, outcome, log) {
|
|
38
|
+
const before = read(recordPath(methodId, outcome.id));
|
|
39
|
+
write({ judgment: outcome.id, method_id: methodId, file: resolve(file), status: 'done', started_at: before?.started_at ?? new Date().toISOString(), finished_at: new Date().toISOString(), outcome, ...(log ? { log } : {}), shown: false });
|
|
40
|
+
}
|
|
41
|
+
export function saveError(methodId, file, id, error, log) {
|
|
42
|
+
const before = read(recordPath(methodId, id));
|
|
43
|
+
write({ judgment: id, method_id: methodId, file: resolve(file), status: 'stopped', started_at: before?.started_at ?? new Date().toISOString(), finished_at: new Date().toISOString(), error, ...(log ? { log } : {}), shown: false });
|
|
44
|
+
}
|
|
45
|
+
/** A line that is not about one correction (a pruning change; a fix that came back). */
|
|
46
|
+
export function saveNotice(methodId, file, kind, line) {
|
|
47
|
+
const id = `${kind}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
48
|
+
write({ judgment: id, method_id: methodId, file: resolve(file), status: 'done', kind, line, started_at: new Date().toISOString(), finished_at: new Date().toISOString(), shown: false });
|
|
49
|
+
}
|
|
50
|
+
export function storedOutcome(methodId, id) { return read(recordPath(methodId, id)); }
|
|
51
|
+
const alive = (pid) => { if (!pid)
|
|
52
|
+
return true; try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return error.code !== 'ESRCH';
|
|
58
|
+
} };
|
|
59
|
+
/** Every stored record, of one Method or of all; a working record whose process stopped reads as stopped. */
|
|
60
|
+
export function storedOutcomes(methodId) {
|
|
61
|
+
const root = join(methodCache(), 'fixes');
|
|
62
|
+
const methods = methodId ? [methodId] : (() => { try {
|
|
63
|
+
return readdirSync(root);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
} })();
|
|
68
|
+
const records = [];
|
|
69
|
+
for (const id of methods) {
|
|
70
|
+
let names = [];
|
|
71
|
+
try {
|
|
72
|
+
names = readdirSync(outcomesDir(id)).filter(name => name.endsWith('.json'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
const record = read(join(outcomesDir(id), name));
|
|
79
|
+
if (!record)
|
|
80
|
+
continue;
|
|
81
|
+
records.push(record.status === 'working' && !alive(record.pid) ? { ...record, status: 'stopped' } : record);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return records.sort((a, b) => a.started_at.localeCompare(b.started_at));
|
|
85
|
+
}
|
|
86
|
+
export function markShown(record) { const now = read(recordPath(record.method_id, record.judgment)); if (now)
|
|
87
|
+
write({ ...now, shown: true }); }
|
|
88
|
+
/** The lines of a record: the person's line, the tested output after a fix, and for a question the agent's next step. */
|
|
89
|
+
export function recordLines(record, options = {}) {
|
|
90
|
+
if (record.line)
|
|
91
|
+
return [record.line];
|
|
92
|
+
if (record.status === 'working')
|
|
93
|
+
return [workingLine];
|
|
94
|
+
if (record.status === 'stopped')
|
|
95
|
+
return [record.error ? errorLine(record.error) : stoppedLine];
|
|
96
|
+
const o = record.outcome, lines = [outcomeLine(o)];
|
|
97
|
+
if (o.outcome === 'question')
|
|
98
|
+
lines.push(answerNext(record.judgment));
|
|
99
|
+
const shown = o.tested_output;
|
|
100
|
+
if (o.outcome === 'fixed' && shown && !options.brief) {
|
|
101
|
+
const parts = [...(shown.result !== undefined && shown.result !== null ? [clip(valueText(shown.result))] : []), ...Object.entries(shown.files ?? {}).map(([name, text]) => `${name}:\n${clip(text)}`)];
|
|
102
|
+
if (parts.length)
|
|
103
|
+
lines.push(testedOutputHeading, ...parts);
|
|
104
|
+
if (shown.held_back?.length)
|
|
105
|
+
lines.push(heldBackHeading, ...shown.held_back.map(call => clip(valueText(call.args))));
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
const clip = (text, max = 4000) => text.length > max ? `${text.slice(0, max)}…` : text;
|
|
110
|
+
/** The JSON of a record for --json: the outcome as the agent reads it, with the person's line. */
|
|
111
|
+
export const recordJson = (record) => ({ judgment: record.judgment, status: record.status, ...(record.outcome ? { outcome: record.outcome } : {}), ...(record.error ? { error: record.error } : {}),
|
|
112
|
+
...(record.kind ? { kind: record.kind } : {}), lines: recordLines(record), ...(record.outcome?.outcome === 'question' ? { next: `method answer ${record.judgment} "THEIR WORDS"` } : {}) });
|
|
113
|
+
/** The lines of the outcomes of a Method that nobody has seen yet (without the tested output), marked seen. */
|
|
114
|
+
export function unshownLines(methodId) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
for (const record of storedOutcomes(methodId).filter(r => !r.shown && r.status !== 'working')) {
|
|
117
|
+
lines.push(...recordLines(record, { brief: true }));
|
|
118
|
+
markShown(record);
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
/** Starts the fix of these corrections in its own process; tests replace it. */
|
|
123
|
+
export const fixStarter = {
|
|
124
|
+
async start(file, methodId, ids, server, options = {}) {
|
|
125
|
+
const folder = fixFolder(methodId);
|
|
126
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
127
|
+
const out = openSync(join(folder, 'fix.log'), 'a', 0o600);
|
|
128
|
+
const suffix = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
129
|
+
const entry = fileURLToPath(new URL(`./method${suffix}`, import.meta.url));
|
|
130
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, '__fix', resolve(file), '--server', server, ...ids.flatMap(id => ['--judgment', id]), ...(options.prune ? ['--prune'] : [])], { detached: true, stdio: ['ignore', out, out], cwd: process.cwd(), env: { ...process.env, METHOD_RUN_WORKER: '' } });
|
|
131
|
+
closeSync(out);
|
|
132
|
+
await new Promise((yes, no) => { child.once('spawn', yes); child.once('error', no); });
|
|
133
|
+
if (child.pid)
|
|
134
|
+
setWorker(methodId, ids, child.pid);
|
|
135
|
+
child.unref();
|
|
136
|
+
},
|
|
137
|
+
/** Judges the run's output against the corrections that recent fixes answered, in its own process. */
|
|
138
|
+
watch(file, runDir, server) {
|
|
139
|
+
try {
|
|
140
|
+
const suffix = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
141
|
+
const entry = fileURLToPath(new URL(`./method${suffix}`, import.meta.url));
|
|
142
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, '__watch', resolve(file), resolve(runDir), '--server', server], { detached: true, stdio: 'ignore', cwd: process.cwd(), env: { ...process.env, METHOD_RUN_WORKER: '' } });
|
|
143
|
+
child.on('error', () => { });
|
|
144
|
+
child.unref();
|
|
145
|
+
}
|
|
146
|
+
catch { /* watching never delays or fails a run */ }
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* `method wait [JUDGMENT_ID] [--timeout SECONDS] [--json]`: wait for the fix of a correction (or of every correction
|
|
151
|
+
* that is being fixed on this computer) and print what it did. A wait that ends before the fix says so in one line;
|
|
152
|
+
* the fix goes on, and method wait can run again.
|
|
153
|
+
*/
|
|
154
|
+
export async function waitCommand(args, options = {}) {
|
|
155
|
+
const { parseArgs } = await import('node:util');
|
|
156
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { timeout: { type: 'string' }, json: { type: 'boolean' } } });
|
|
157
|
+
const id = positionals[0];
|
|
158
|
+
const timeout = values.timeout !== undefined ? Number(values.timeout) : 100;
|
|
159
|
+
if (!Number.isFinite(timeout) || timeout < 0)
|
|
160
|
+
throw Error('Use method wait [JUDGMENT_ID] [--timeout SECONDS].');
|
|
161
|
+
const deadline = Date.now() + timeout * 1000;
|
|
162
|
+
const chosen = () => storedOutcomes().filter(r => id ? r.judgment === id : !r.shown);
|
|
163
|
+
let records = chosen();
|
|
164
|
+
if (id && !records.length)
|
|
165
|
+
throw Error(`This computer is not fixing ${id}. Its outcome is on the run's page on the dashboard.`);
|
|
166
|
+
while (records.some(r => r.status === 'working') && Date.now() < deadline) {
|
|
167
|
+
await new Promise(done => setTimeout(done, options.pollMs ?? 500));
|
|
168
|
+
records = chosen();
|
|
169
|
+
}
|
|
170
|
+
const finished = records.filter(r => r.status !== 'working'), working = records.filter(r => r.status === 'working');
|
|
171
|
+
for (const record of finished)
|
|
172
|
+
markShown(record);
|
|
173
|
+
if (values.json) {
|
|
174
|
+
process.stdout.write(JSON.stringify({ outcomes: records.map(recordJson) }, null, 2) + '\n');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const lines = finished.flatMap(record => recordLines(record));
|
|
178
|
+
if (working.length)
|
|
179
|
+
lines.push((await import('./fix-lines.js')).stillWorkingLine);
|
|
180
|
+
if (!records.length)
|
|
181
|
+
lines.push((await import('./fix-lines.js')).nothingWaitingLine);
|
|
182
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
183
|
+
}
|
|
184
|
+
// The global pointer to the Method file of the last change on this computer, for method undo with no file.
|
|
185
|
+
const lastChangeFile = () => join(methodCache(), 'fixes', 'last-change.json');
|
|
186
|
+
export function recordLastChange(file) { mkdirSync(join(methodCache(), 'fixes'), { recursive: true, mode: 0o700 }); writePrivateJson(lastChangeFile(), { file: resolve(file) }); }
|
|
187
|
+
export function lastChangedFile() { try {
|
|
188
|
+
const file = JSON.parse(readFileSync(lastChangeFile(), 'utf8')).file;
|
|
189
|
+
return existsSync(file) ? file : undefined;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
} }
|
|
194
|
+
export function readFixState(methodId) { try {
|
|
195
|
+
return { fixes_since_prune: 0, agent: null, ...JSON.parse(readFileSync(join(fixFolder(methodId), 'state.json'), 'utf8')) };
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { fixes_since_prune: 0, agent: null };
|
|
199
|
+
} }
|
|
200
|
+
export function writeFixState(methodId, state) { mkdirSync(fixFolder(methodId), { recursive: true, mode: 0o700 }); writeFileSync(join(fixFolder(methodId), 'state.json'), JSON.stringify(state), { mode: 0o600 }); }
|
|
201
|
+
export function forgetOutcome(methodId, id) { rmSync(recordPath(methodId, id), { force: true }); }
|