@itmetsam/samurai-framework 0.22.6 → 0.22.7

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.
@@ -1,8 +1,9 @@
1
1
  import type { AgentOptions, AgentSession, PlanOptions, PlanSession } from "./types.js";
2
2
  export { DEFAULT_LIMITS, resolveLimits, type AgentLimits } from "./limits.js";
3
3
  export { assertModelObject, type AgentModel } from "./model.js";
4
- export { parsePlan, type ParsedPlan } from "./plan/plan-file.js";
5
- export { defaultPlanFile } from "./plan/plan-path.js";
4
+ export { splitFrontMatter } from "./plan/front-matter.js";
5
+ export { formatPlan, MAX_PLAN_BYTES, parsePlan, type ParsedPlan, type PlanDocument, type PlanQuestion, } from "./plan/plan-file.js";
6
+ export { defaultPlanFile, listPlans } from "./plan/plan-path.js";
6
7
  export { readPlan } from "./plan/read-plan.js";
7
8
  export { defaultModelSpec, ModelSetupError, modelFromSpec, } from "./providers.js";
8
9
  export type { AgentEndReason, AgentEvent, AgentOptions, AgentResult, AgentSession, PlanOptions, PlanResult, PlanSession, } from "./types.js";
@@ -2,8 +2,9 @@ import { startPlan } from "./plan/run.js";
2
2
  import { startAgent } from "./run.js";
3
3
  export { DEFAULT_LIMITS, resolveLimits } from "./limits.js";
4
4
  export { assertModelObject } from "./model.js";
5
- export { parsePlan } from "./plan/plan-file.js";
6
- export { defaultPlanFile } from "./plan/plan-path.js";
5
+ export { splitFrontMatter } from "./plan/front-matter.js";
6
+ export { formatPlan, MAX_PLAN_BYTES, parsePlan, } from "./plan/plan-file.js";
7
+ export { defaultPlanFile, listPlans } from "./plan/plan-path.js";
7
8
  export { readPlan } from "./plan/read-plan.js";
8
9
  export { defaultModelSpec, ModelSetupError, modelFromSpec, } from "./providers.js";
9
10
  /**
@@ -1,5 +1,6 @@
1
1
  /** A plan file may not be larger than this (bytes), for the planner and for `generate --plan` */
2
2
  export declare const MAX_PLAN_BYTES: number;
3
+ /** One step of a plan, with what to check after it */
3
4
  interface PlanStep {
4
5
  text: string;
5
6
  expect?: string;
@@ -14,12 +15,35 @@ interface ParsedPlan {
14
15
  notDone: string[];
15
16
  /** Every open question, answered or not */
16
17
  questions: string[];
18
+ /** Every open question with its answer, if it has one; what `formatPlan` takes back */
19
+ questionList: PlanQuestion[];
17
20
  /** The questions that have an `Answer:` line, with the answer; a question with an empty answer is not here */
18
21
  answers: {
19
22
  question: string;
20
23
  answer: string;
21
24
  }[];
22
25
  }
26
+ /** A plan as `formatPlan` writes it and `parsePlan` reads it back */
27
+ interface PlanDocument {
28
+ title: string;
29
+ goal: string;
30
+ start: string;
31
+ steps: PlanStep[];
32
+ seen: string[];
33
+ notDone: string[];
34
+ /** An answered question is written with an indented `Answer:` line below it */
35
+ questions: PlanQuestion[];
36
+ }
37
+ interface PlanQuestion {
38
+ text: string;
39
+ answer?: string;
40
+ }
41
+ /**
42
+ * Writes a plan as Markdown, the format the planner writes and `parsePlan` reads. Every text is masked and made one
43
+ * line. `front` is a raw front matter block (ending with a new line, as `splitFrontMatter` returns it) placed first.
44
+ * Throws when the result is larger than `MAX_PLAN_BYTES`.
45
+ */
46
+ export declare function formatPlan(plan: PlanDocument, front?: string): string;
23
47
  /**
24
48
  * A plan file being written. The whole file is written again after every call, so it is valid Markdown with its
25
49
  * four headings whenever the run stops. Texts are masked and made one line long.
@@ -52,7 +76,7 @@ declare class PlanFile {
52
76
  /** Writes the first version of the file */
53
77
  create(): void;
54
78
  }
55
- export type { PlanFile, ParsedPlan };
79
+ export type { PlanFile, ParsedPlan, PlanDocument, PlanQuestion };
56
80
  /** Starts a plan file: the headings are written at once. An existing file is an error unless `force` is set */
57
81
  export declare function createPlan(input: {
58
82
  goal: string;
@@ -16,6 +16,48 @@ function bullets(items) {
16
16
  ? items.map((item) => `- ${item}`).join("\n")
17
17
  : `- ${NONE}`;
18
18
  }
19
+ /**
20
+ * Writes a plan as Markdown, the format the planner writes and `parsePlan` reads. Every text is masked and made one
21
+ * line. `front` is a raw front matter block (ending with a new line, as `splitFrontMatter` returns it) placed first.
22
+ * Throws when the result is larger than `MAX_PLAN_BYTES`.
23
+ */
24
+ export function formatPlan(plan, front = "") {
25
+ const steps = plan.steps.map((step, i) => {
26
+ const expect = step.expect === undefined ? "" : oneLine(step.expect);
27
+ return `${i + 1}. ${oneLine(step.text)}${expect !== "" ? `\n Expect: ${expect}` : ""}`;
28
+ });
29
+ const questions = plan.questions.map((q) => {
30
+ const answer = q.answer === undefined ? "" : oneLine(q.answer);
31
+ return `- ${oneLine(q.text)}${answer !== "" ? `\n Answer: ${answer}` : ""}`;
32
+ });
33
+ const text = [
34
+ ...(front === "" ? [] : [front.replace(/\n$/, "")]),
35
+ `# Plan: ${oneLine(plan.title)}`,
36
+ "",
37
+ `Goal: ${oneLine(plan.goal)}`,
38
+ `Start: ${oneLine(plan.start)}`,
39
+ "",
40
+ "## Steps",
41
+ "",
42
+ steps.length > 0 ? steps.join("\n") : `- ${NONE}`,
43
+ "",
44
+ "## Seen on the page",
45
+ "",
46
+ bullets(plan.seen.map(oneLine)),
47
+ "",
48
+ "## Not done",
49
+ "",
50
+ bullets(plan.notDone.map(oneLine)),
51
+ "",
52
+ "## Open questions",
53
+ "",
54
+ questions.length > 0 ? questions.join("\n") : `- ${NONE}`,
55
+ "",
56
+ ].join("\n");
57
+ if (Buffer.byteLength(text) > MAX_PLAN_BYTES)
58
+ throw new Error(`the plan file is full (${MAX_PLAN_BYTES / 1024} KB); finish with what it holds, or shorten the next entry`);
59
+ return text;
60
+ }
19
61
  /**
20
62
  * A plan file being written. The whole file is written again after every call, so it is valid Markdown with its
21
63
  * four headings whenever the run stops. Texts are masked and made one line long.
@@ -48,37 +90,19 @@ class PlanFile {
48
90
  }
49
91
  }
50
92
  render(front) {
51
- const steps = this.steps.map((step, i) => `${i + 1}. ${step.text}${step.expect !== undefined ? `\n Expect: ${step.expect}` : ""}`);
52
- return [
53
- ...(front === "" ? [] : [front.replace(/\n$/, "")]),
54
- `# Plan: ${this.title}`,
55
- "",
56
- `Goal: ${this.goal}`,
57
- `Start: ${this.start}`,
58
- "",
59
- "## Steps",
60
- "",
61
- steps.length > 0 ? steps.join("\n") : `- ${NONE}`,
62
- "",
63
- "## Seen on the page",
64
- "",
65
- bullets(this.seenItems),
66
- "",
67
- "## Not done",
68
- "",
69
- bullets(this.notDoneItems),
70
- "",
71
- "## Open questions",
72
- "",
73
- bullets(this.questionItems),
74
- "",
75
- ].join("\n");
93
+ return formatPlan({
94
+ title: this.title,
95
+ goal: this.goal,
96
+ start: this.start,
97
+ steps: this.steps,
98
+ seen: this.seenItems,
99
+ notDone: this.notDoneItems,
100
+ questions: this.questionItems.map((text) => ({ text })),
101
+ }, front);
76
102
  }
77
103
  /** Writes the file; refuses, and keeps the file as it was, when it would pass the size limit */
78
104
  save() {
79
105
  const text = this.render(this.frontMatter());
80
- if (Buffer.byteLength(text) > MAX_PLAN_BYTES)
81
- throw new Error(`the plan file is full (${MAX_PLAN_BYTES / 1024} KB); finish with what it holds, or shorten the next entry`);
82
106
  mkdirSync(path.dirname(this.file), { recursive: true });
83
107
  writeFileSync(this.file, text);
84
108
  }
@@ -167,6 +191,7 @@ export function parsePlan(text) {
167
191
  seen: [],
168
192
  notDone: [],
169
193
  questions: [],
194
+ questionList: [],
170
195
  answers: [],
171
196
  };
172
197
  let section;
@@ -215,13 +240,18 @@ export function parsePlan(text) {
215
240
  const asked = plan.questions[plan.questions.length - 1];
216
241
  if (answer) {
217
242
  const text = answer[1].trim();
218
- if (asked !== undefined && text !== "")
243
+ if (asked !== undefined && text !== "") {
219
244
  plan.answers.push({ question: asked, answer: text });
245
+ plan.questionList[plan.questionList.length - 1].answer = text;
246
+ }
220
247
  continue;
221
248
  }
222
249
  const item = BULLET.exec(line)?.[1]?.trim();
223
- if (item !== undefined && item !== NONE)
250
+ if (item !== undefined && item !== NONE) {
224
251
  plan[section].push(item);
252
+ if (section === "questions")
253
+ plan.questionList.push({ text: item });
254
+ }
225
255
  }
226
256
  if (!hasSteps)
227
257
  throw new Error('parsePlan(): the plan has no "## Steps" heading');
@@ -6,3 +6,8 @@ export declare function slugOf(goal: string): string;
6
6
  * created. Returns the path relative to the project folder, with `/` separators.
7
7
  */
8
8
  export declare function defaultPlanFile(goal: string): Promise<string>;
9
+ /**
10
+ * The plan files (`*.plan.md`) below the config's `plansDir`, searched in all sub-folders: paths relative to the
11
+ * project folder, with `/` separators, sorted. A missing folder gives an empty list.
12
+ */
13
+ export declare function listPlans(): Promise<string[]>;
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loadRawConfig, projectDir } from "../../config/config.js";
4
4
  import { DEFAULT_PLANS_DIR } from "../../config/defaults.js";
@@ -44,3 +44,19 @@ export async function defaultPlanFile(goal) {
44
44
  .join("/");
45
45
  }
46
46
  }
47
+ /**
48
+ * The plan files (`*.plan.md`) below the config's `plansDir`, searched in all sub-folders: paths relative to the
49
+ * project folder, with `/` separators, sorted. A missing folder gives an empty list.
50
+ */
51
+ export async function listPlans() {
52
+ const folder = path.resolve(projectDir(), await plansDirOf());
53
+ if (!existsSync(folder))
54
+ return [];
55
+ return readdirSync(folder, { recursive: true, encoding: "utf8" })
56
+ .filter((name) => name.endsWith(".plan.md"))
57
+ .map((name) => path
58
+ .relative(projectDir(), path.join(folder, name))
59
+ .split(path.sep)
60
+ .join("/"))
61
+ .sort();
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.22.6",
3
+ "version": "0.22.7",
4
4
  "description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
5
5
  "main": "dist/api.js",
6
6
  "exports": {