pagegraph 0.8.1 → 0.8.3

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.1";
35713
+ var version = "0.8.3";
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;");
@@ -43433,7 +43433,7 @@ const TranscriptSchema = Struct({ messages: ArraySchema(Unknown) });
43433
43433
  const decodeTextPart = decodeUnknownOption(TextPartSchema);
43434
43434
  const decodeAssistantMessage = decodeUnknownOption(AssistantMessageSchema);
43435
43435
  const decodeTranscript = decodeUnknownSync(TranscriptSchema);
43436
- const parseState = (transcript) => {
43436
+ const parseWorkflowState = (transcript) => {
43437
43437
  const candidates = decodeTranscript(transcript).messages.flatMap((message) => {
43438
43438
  const assistant = decodeAssistantMessage(message);
43439
43439
  if (isNone(assistant)) return [];
@@ -43452,6 +43452,25 @@ const parseState = (transcript) => {
43452
43452
  }
43453
43453
  throw new Error("The SEO agent did not return a valid workflow JSON object.");
43454
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(" ");
43455
43474
  const decodeToolPart = decodeUnknownOption(Struct({
43456
43475
  type: Literal("tool"),
43457
43476
  id: String$6,
@@ -43479,6 +43498,31 @@ const toolText = (value) => {
43479
43498
  return Object.values(record).map(toolText).join("\n");
43480
43499
  };
43481
43500
  const pathsIn = (value) => [...toolText(value).matchAll(/tools\.([A-Za-z0-9_.-]+)/g)].map((match) => match[1] ?? "").filter(Boolean);
43501
+ const toolPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;
43502
+ const pathsInSearchOutput = (value) => {
43503
+ const found = /* @__PURE__ */ new Set();
43504
+ const visit = (current, pathField = false) => {
43505
+ if (typeof current === "string") {
43506
+ if (pathField) {
43507
+ const candidate = current.startsWith("tools.") ? current.slice(6) : current;
43508
+ if (toolPathPattern.test(candidate)) found.add(candidate);
43509
+ }
43510
+ try {
43511
+ visit(JSON.parse(current));
43512
+ } catch {}
43513
+ return;
43514
+ }
43515
+ if (Array.isArray(current)) {
43516
+ for (const item of current) visit(item);
43517
+ return;
43518
+ }
43519
+ if (current === null || typeof current !== "object") return;
43520
+ const record = current;
43521
+ for (const [key, nested] of Object.entries(record)) visit(nested, key === "path");
43522
+ };
43523
+ visit(value);
43524
+ return [...found];
43525
+ };
43482
43526
  const evidenceRecord = (part) => ({
43483
43527
  tool: part.name,
43484
43528
  input: part.state.input ?? null,
@@ -43492,7 +43536,7 @@ const collectExecutorEvidence = (transcript) => {
43492
43536
  const inputPaths = pathsIn(part.state.input);
43493
43537
  const inputText = toolText(part.state.input);
43494
43538
  if (part.name === "executor_search" || part.name === "executor.search" || inputPaths.some((path) => path === "executor.search")) {
43495
- for (const path of pathsIn(part.state.content)) if (path !== "executor.search") discovered.add(path);
43539
+ for (const path of pathsInSearchOutput(part.state.content)) if (path !== "executor.search") discovered.add(path);
43496
43540
  searches.push(evidenceRecord(part));
43497
43541
  continue;
43498
43542
  }
@@ -43515,14 +43559,14 @@ const interactionEventError = (event, sessionId) => {
43515
43559
  if (form !== null && typeof form === "object" && form["sessionID"] === sessionId) return /* @__PURE__ */ new Error("OpenCode requested a form; embedded PageGraph workflows are noninteractive.");
43516
43560
  }
43517
43561
  };
43518
- const waitForIdle = async (host, sessionId, timeoutMs, after) => {
43562
+ const waitForIdle = async (host, sessionId, options) => {
43519
43563
  const controller = new AbortController();
43520
- const timer = setTimeout(() => controller.abort(), timeoutMs);
43564
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs);
43521
43565
  try {
43522
43566
  for await (const event of host.sessions.log({
43523
43567
  sessionID: sessionId,
43524
43568
  follow: true,
43525
- after
43569
+ after: options.after
43526
43570
  }, { signal: controller.signal })) {
43527
43571
  const interactionError = interactionEventError(event, sessionId);
43528
43572
  if (interactionError !== void 0) throw interactionError;
@@ -43530,6 +43574,9 @@ const waitForIdle = async (host, sessionId, timeoutMs, after) => {
43530
43574
  if (event.type === "session.execution.failed" && event.data.sessionID === sessionId) throw new Error(`OpenCode session failed: ${JSON.stringify(event.data.error)}`);
43531
43575
  if (event.type === "session.execution.interrupted" && event.data.sessionID === sessionId) throw new Error(`OpenCode session was interrupted: ${event.data.reason}.`);
43532
43576
  }
43577
+ } catch (cause) {
43578
+ if (controller.signal.aborted) throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs ?? options.timeoutMs}ms.`, { cause });
43579
+ throw cause;
43533
43580
  } finally {
43534
43581
  clearTimeout(timer);
43535
43582
  }
@@ -43556,22 +43603,62 @@ const acquireWorkflowHost = async (options) => {
43556
43603
  sessionID: sessionId,
43557
43604
  permissions: [...workflowOptions.permissions]
43558
43605
  });
43606
+ const configuredTimeoutMs = options.timeoutMs ?? 18e4;
43607
+ const deadline = Date.now() + configuredTimeoutMs;
43608
+ const remaining = () => Math.max(0, deadline - Date.now());
43559
43609
  await host.sessions.prompt({
43560
43610
  sessionID: sessionId,
43561
43611
  text: prompt,
43562
43612
  skills: workflowOptions.skills.map((id) => ({ id }))
43563
43613
  });
43564
- const cursor = await waitForIdle(host, sessionId, options.timeoutMs ?? 18e4, cursors.get(sessionId));
43614
+ let cursor = await waitForIdle(host, sessionId, {
43615
+ timeoutMs: remaining(),
43616
+ configuredTimeoutMs,
43617
+ after: cursors.get(sessionId)
43618
+ });
43565
43619
  cursors.set(sessionId, cursor);
43566
- const transcript = await host.sessions.export({
43620
+ let transcript = await host.sessions.export({
43567
43621
  sessionID: sessionId,
43568
43622
  sanitize: false
43569
43623
  });
43624
+ const parsed = await parseWorkflowStateWithRepair(transcript, async () => {
43625
+ const priorPermissions = [...workflowOptions.permissions ?? []];
43626
+ await host.sessions.update({
43627
+ sessionID: sessionId,
43628
+ permissions: [{
43629
+ action: "*",
43630
+ resource: "*",
43631
+ effect: "deny"
43632
+ }]
43633
+ });
43634
+ try {
43635
+ await host.sessions.prompt({
43636
+ sessionID: sessionId,
43637
+ text: STATE_REPAIR_PROMPT
43638
+ });
43639
+ cursor = await waitForIdle(host, sessionId, {
43640
+ timeoutMs: remaining(),
43641
+ configuredTimeoutMs,
43642
+ after: cursor
43643
+ });
43644
+ cursors.set(sessionId, cursor);
43645
+ transcript = await host.sessions.export({
43646
+ sessionID: sessionId,
43647
+ sanitize: false
43648
+ });
43649
+ return transcript;
43650
+ } finally {
43651
+ await host.sessions.update({
43652
+ sessionID: sessionId,
43653
+ permissions: priorPermissions
43654
+ });
43655
+ }
43656
+ });
43570
43657
  return {
43571
- state: parseState(transcript),
43658
+ state: parsed.state,
43572
43659
  sessionId,
43573
- transcript,
43574
- executor: collectExecutorEvidence(transcript)
43660
+ transcript: parsed.transcript,
43661
+ executor: collectExecutorEvidence(parsed.transcript)
43575
43662
  };
43576
43663
  };
43577
43664
  return {
@@ -43588,7 +43675,7 @@ const acquireWorkflowHost = async (options) => {
43588
43675
  permissions: workflowOptions.permissions === void 0 ? void 0 : [...workflowOptions.permissions]
43589
43676
  });
43590
43677
  const result = await complete(session.id, prompt, workflowOptions);
43591
- if (result.executor.searches.length === 0 || result.executor.calls.length === 0) throw new Error("The SEO agent returned without searching Executor and calling a discovered tool.");
43678
+ if (result.executor.searches.length === 0 || result.executor.calls.length === 0) throw new Error(`The SEO agent returned without the required Executor evidence (searches: ${result.executor.searches.length}, calls: ${result.executor.calls.length}).`);
43592
43679
  return result;
43593
43680
  },
43594
43681
  continue: (sessionId, prompt, workflowOptions) => complete(sessionId, prompt, workflowOptions),