pagegraph 0.8.0 → 0.8.2

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/dist/cli.js CHANGED
@@ -35710,7 +35710,7 @@ const runWith = (command, config) => {
35710
35710
  };
35711
35711
  //#endregion
35712
35712
  //#region package.json
35713
- var version = "0.8.0";
35713
+ var version = "0.8.2";
35714
35714
  //#endregion
35715
35715
  //#region src/core/projections.ts
35716
35716
  const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -43389,6 +43389,21 @@ const createWorkflowMutationPolicy = (options) => {
43389
43389
  };
43390
43390
  //#endregion
43391
43391
  //#region src/workflows/opencode.ts
43392
+ const isActiveExecutorPlugin = (plugin) => JSON.stringify(plugin.source).toLowerCase().includes("executor") && plugin.state.status === "active";
43393
+ const waitForActiveExecutorPlugin = async (options) => {
43394
+ const timeoutMs = options.timeoutMs ?? 3e4;
43395
+ const pollMs = options.pollMs ?? 250;
43396
+ const now = options.now ?? Date.now;
43397
+ const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
43398
+ const deadline = now() + timeoutMs;
43399
+ do {
43400
+ if ((await options.list()).some(isActiveExecutorPlugin)) return true;
43401
+ const remaining = deadline - now();
43402
+ if (remaining <= 0) return false;
43403
+ await sleep(Math.min(pollMs, remaining));
43404
+ if (now() >= deadline) return false;
43405
+ } while (true);
43406
+ };
43392
43407
  const assertWorkflowSkillsAvailable = (configDirectory, skills) => {
43393
43408
  const missing = skills.flatMap((skill) => [resolve(configDirectory, "skills", skill, "SKILL.md"), resolve(configDirectory, "skills", skill, "references", "executor.md")].filter((file) => !existsSync(file)));
43394
43409
  if (missing.length === 0) return;
@@ -43418,7 +43433,7 @@ const TranscriptSchema = Struct({ messages: ArraySchema(Unknown) });
43418
43433
  const decodeTextPart = decodeUnknownOption(TextPartSchema);
43419
43434
  const decodeAssistantMessage = decodeUnknownOption(AssistantMessageSchema);
43420
43435
  const decodeTranscript = decodeUnknownSync(TranscriptSchema);
43421
- const parseState = (transcript) => {
43436
+ const parseWorkflowState = (transcript) => {
43422
43437
  const candidates = decodeTranscript(transcript).messages.flatMap((message) => {
43423
43438
  const assistant = decodeAssistantMessage(message);
43424
43439
  if (isNone(assistant)) return [];
@@ -43437,6 +43452,25 @@ const parseState = (transcript) => {
43437
43452
  }
43438
43453
  throw new Error("The SEO agent did not return a valid workflow JSON object.");
43439
43454
  };
43455
+ const parseWorkflowStateWithRepair = async (transcript, repair) => {
43456
+ try {
43457
+ return {
43458
+ state: parseWorkflowState(transcript),
43459
+ transcript
43460
+ };
43461
+ } catch {
43462
+ const repaired = await repair();
43463
+ return {
43464
+ state: parseWorkflowState(repaired),
43465
+ transcript: repaired
43466
+ };
43467
+ }
43468
+ };
43469
+ const STATE_REPAIR_PROMPT = [
43470
+ "Your previous turn did not end with the workflow JSON object requested by the original prompt.",
43471
+ "Do not call tools, add commentary, or wrap the response in Markdown.",
43472
+ "Return only one valid JSON object matching the original JSON Schema, using the evidence already collected."
43473
+ ].join(" ");
43440
43474
  const decodeToolPart = decodeUnknownOption(Struct({
43441
43475
  type: Literal("tool"),
43442
43476
  id: String$6,
@@ -43500,14 +43534,14 @@ const interactionEventError = (event, sessionId) => {
43500
43534
  if (form !== null && typeof form === "object" && form["sessionID"] === sessionId) return /* @__PURE__ */ new Error("OpenCode requested a form; embedded PageGraph workflows are noninteractive.");
43501
43535
  }
43502
43536
  };
43503
- const waitForIdle = async (host, sessionId, timeoutMs, after) => {
43537
+ const waitForIdle = async (host, sessionId, options) => {
43504
43538
  const controller = new AbortController();
43505
- const timer = setTimeout(() => controller.abort(), timeoutMs);
43539
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs);
43506
43540
  try {
43507
43541
  for await (const event of host.sessions.log({
43508
43542
  sessionID: sessionId,
43509
43543
  follow: true,
43510
- after
43544
+ after: options.after
43511
43545
  }, { signal: controller.signal })) {
43512
43546
  const interactionError = interactionEventError(event, sessionId);
43513
43547
  if (interactionError !== void 0) throw interactionError;
@@ -43515,6 +43549,9 @@ const waitForIdle = async (host, sessionId, timeoutMs, after) => {
43515
43549
  if (event.type === "session.execution.failed" && event.data.sessionID === sessionId) throw new Error(`OpenCode session failed: ${JSON.stringify(event.data.error)}`);
43516
43550
  if (event.type === "session.execution.interrupted" && event.data.sessionID === sessionId) throw new Error(`OpenCode session was interrupted: ${event.data.reason}.`);
43517
43551
  }
43552
+ } catch (cause) {
43553
+ if (controller.signal.aborted) throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs ?? options.timeoutMs}ms.`, { cause });
43554
+ throw cause;
43518
43555
  } finally {
43519
43556
  clearTimeout(timer);
43520
43557
  }
@@ -43533,7 +43570,7 @@ const acquireWorkflowHost = async (options) => {
43533
43570
  });
43534
43571
  try {
43535
43572
  const location = { directory: options.root };
43536
- if ((await host.plugin.list({ location })).data.find((plugin) => JSON.stringify(plugin.source).toLowerCase().includes("executor") && plugin.state.status === "active") === void 0) throw new Error(`No active Executor plugin was discovered in ${configDirectory}; agentic SEO workflows require Executor.`);
43573
+ if (!await waitForActiveExecutorPlugin({ list: async () => (await host.plugin.list({ location })).data })) throw new Error(`No active Executor plugin was discovered in ${configDirectory}; agentic SEO workflows require Executor.`);
43537
43574
  const cursors = /* @__PURE__ */ new Map();
43538
43575
  const complete = async (sessionId, prompt, workflowOptions) => {
43539
43576
  assertWorkflowSkillsAvailable(configDirectory, workflowOptions.skills);
@@ -43541,22 +43578,62 @@ const acquireWorkflowHost = async (options) => {
43541
43578
  sessionID: sessionId,
43542
43579
  permissions: [...workflowOptions.permissions]
43543
43580
  });
43581
+ const configuredTimeoutMs = options.timeoutMs ?? 18e4;
43582
+ const deadline = Date.now() + configuredTimeoutMs;
43583
+ const remaining = () => Math.max(0, deadline - Date.now());
43544
43584
  await host.sessions.prompt({
43545
43585
  sessionID: sessionId,
43546
43586
  text: prompt,
43547
43587
  skills: workflowOptions.skills.map((id) => ({ id }))
43548
43588
  });
43549
- const cursor = await waitForIdle(host, sessionId, options.timeoutMs ?? 18e4, cursors.get(sessionId));
43589
+ let cursor = await waitForIdle(host, sessionId, {
43590
+ timeoutMs: remaining(),
43591
+ configuredTimeoutMs,
43592
+ after: cursors.get(sessionId)
43593
+ });
43550
43594
  cursors.set(sessionId, cursor);
43551
- const transcript = await host.sessions.export({
43595
+ let transcript = await host.sessions.export({
43552
43596
  sessionID: sessionId,
43553
43597
  sanitize: false
43554
43598
  });
43599
+ const parsed = await parseWorkflowStateWithRepair(transcript, async () => {
43600
+ const priorPermissions = [...workflowOptions.permissions ?? []];
43601
+ await host.sessions.update({
43602
+ sessionID: sessionId,
43603
+ permissions: [{
43604
+ action: "*",
43605
+ resource: "*",
43606
+ effect: "deny"
43607
+ }]
43608
+ });
43609
+ try {
43610
+ await host.sessions.prompt({
43611
+ sessionID: sessionId,
43612
+ text: STATE_REPAIR_PROMPT
43613
+ });
43614
+ cursor = await waitForIdle(host, sessionId, {
43615
+ timeoutMs: remaining(),
43616
+ configuredTimeoutMs,
43617
+ after: cursor
43618
+ });
43619
+ cursors.set(sessionId, cursor);
43620
+ transcript = await host.sessions.export({
43621
+ sessionID: sessionId,
43622
+ sanitize: false
43623
+ });
43624
+ return transcript;
43625
+ } finally {
43626
+ await host.sessions.update({
43627
+ sessionID: sessionId,
43628
+ permissions: priorPermissions
43629
+ });
43630
+ }
43631
+ });
43555
43632
  return {
43556
- state: parseState(transcript),
43633
+ state: parsed.state,
43557
43634
  sessionId,
43558
- transcript,
43559
- executor: collectExecutorEvidence(transcript)
43635
+ transcript: parsed.transcript,
43636
+ executor: collectExecutorEvidence(parsed.transcript)
43560
43637
  };
43561
43638
  };
43562
43639
  return {