codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4
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/CHANGELOG.md +42 -0
- package/README.md +43 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +286 -0
- package/dist/src/operations/service.js +2831 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
import { formatMessageHtml, normalizeResponseFormat } from "../dom/message-format.js";
|
|
2
|
+
import { TURN_OWNERSHIP_SCHEMA_VERSION } from "./turn-ownership.js";
|
|
3
|
+
import { COLLECTOR_TERMINAL_SCHEMA_VERSION } from "./collector.js";
|
|
4
|
+
import { isPlainDataRecord } from "../runtime/value-boundaries.js";
|
|
5
|
+
const ERROR_MESSAGES = {
|
|
6
|
+
page_evaluation_unavailable: "Browser observation requires a read-only page evaluation boundary.",
|
|
7
|
+
page_evaluation_failed: "Browser observation could not complete its bounded DOM transaction.",
|
|
8
|
+
provider_shape_drift: "Browser observation found an unsupported provider DOM shape.",
|
|
9
|
+
missing_identity: "Browser observation is missing a required stable identity.",
|
|
10
|
+
duplicate_identity: "Browser observation found duplicate stable identities.",
|
|
11
|
+
unstable_identity: "Browser observation found an identity that changed within one DOM snapshot.",
|
|
12
|
+
incomplete_dom: "Browser observation found an incomplete or out-of-order conversation DOM.",
|
|
13
|
+
navigation_ambiguous: "Browser observation could not prove one canonical conversation navigation target.",
|
|
14
|
+
branch_ambiguous: "Browser observation could not prove one stable assistant branch.",
|
|
15
|
+
bounded_limit_exceeded: "Browser observation exceeded a bounded DOM or content limit.",
|
|
16
|
+
evidence_digest_failed: "Browser observation could not create privacy-preserving evidence.",
|
|
17
|
+
raw_content_unavailable: "The requested exact terminal assistant content is unavailable."
|
|
18
|
+
};
|
|
19
|
+
export class BrowserObservationError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code) {
|
|
22
|
+
super(ERROR_MESSAGES[code]);
|
|
23
|
+
this.name = "BrowserObservationError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
28
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
|
|
29
|
+
const MAX_TURNS = 256;
|
|
30
|
+
const MAX_ARTIFACTS_PER_TURN = 32;
|
|
31
|
+
const MAX_TEXT_CHARS = 1_000_000;
|
|
32
|
+
const MAX_TOTAL_TEXT_CHARS = 8_000_000;
|
|
33
|
+
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
34
|
+
const MAX_NODES = 32_768;
|
|
35
|
+
const MAX_ATTRIBUTE_LENGTH = 4096;
|
|
36
|
+
const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
|
|
37
|
+
const OPAQUE_URL_PREFIX = "https://opaque.invalid/thread/";
|
|
38
|
+
/**
|
|
39
|
+
* Perform one bounded read-only page transaction and normalize its result.
|
|
40
|
+
* `page.evaluate` is the only browser call made by this adapter.
|
|
41
|
+
*/
|
|
42
|
+
export async function observeBrowserPage(page, options) {
|
|
43
|
+
validateOptions(options);
|
|
44
|
+
if (typeof page.evaluate !== "function") {
|
|
45
|
+
throw new BrowserObservationError("page_evaluation_unavailable");
|
|
46
|
+
}
|
|
47
|
+
const responseContent = options.responseContent ?? "metadata";
|
|
48
|
+
if (responseContent === "include" && options.rawAssistantTurnId === undefined) {
|
|
49
|
+
throw new BrowserObservationError("raw_content_unavailable");
|
|
50
|
+
}
|
|
51
|
+
if (options.rawAssistantTurnId !== undefined
|
|
52
|
+
&& options.terminalAssistantTurnId !== undefined
|
|
53
|
+
&& options.rawAssistantTurnId !== options.terminalAssistantTurnId) {
|
|
54
|
+
throw new BrowserObservationError("raw_content_unavailable");
|
|
55
|
+
}
|
|
56
|
+
const evaluateArgs = {
|
|
57
|
+
maxTurns: boundedPositive(options.maxTurns, MAX_TURNS),
|
|
58
|
+
maxTextChars: boundedPositive(options.maxTextChars, MAX_TEXT_CHARS),
|
|
59
|
+
maxArtifactsPerTurn: boundedPositive(options.maxArtifactsPerTurn, MAX_ARTIFACTS_PER_TURN),
|
|
60
|
+
...((options.terminalAssistantTurnId ?? options.rawAssistantTurnId) === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { captureAssistantTurnId: options.terminalAssistantTurnId ?? options.rawAssistantTurnId }),
|
|
63
|
+
...(options.target.targetLifecycle === "new_pending" ? { allowBlankTask: true } : {})
|
|
64
|
+
};
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = await page.evaluate(readPageObservation, evaluateArgs);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new BrowserObservationError("page_evaluation_failed");
|
|
71
|
+
}
|
|
72
|
+
const parsed = parseRawObservation(raw, evaluateArgs);
|
|
73
|
+
const target = await buildTarget(parsed, options);
|
|
74
|
+
const normalized = await normalizeTurns(parsed.turns, options);
|
|
75
|
+
const postSendDelta = options.baseline === undefined
|
|
76
|
+
? undefined
|
|
77
|
+
: await makePostSendDelta(options.baseline, normalized.userTurns, options.evidenceDigest);
|
|
78
|
+
const snapshotDigest = await digest(options.evidenceDigest, "browser-observation-snapshot", {
|
|
79
|
+
operationId: options.operationId,
|
|
80
|
+
target: targetMaterial(target),
|
|
81
|
+
userTurns: normalized.userTurns.map(turnMaterial),
|
|
82
|
+
assistantTurns: normalized.assistantTurns.map(turnMaterial),
|
|
83
|
+
completeness: parsed.completeness,
|
|
84
|
+
terminalState: parsed.terminalState,
|
|
85
|
+
// A pending target cannot persist a conversation URL yet, but the keyed
|
|
86
|
+
// navigation evidence still makes the blank-task anchor sensitive to a
|
|
87
|
+
// pre-Send navigation change without exposing the raw URL.
|
|
88
|
+
...(options.target.targetLifecycle === "new_pending"
|
|
89
|
+
? { blankTaskNavigationDigest: await digest(options.evidenceDigest, "browser-observation-blank-task-navigation", parsed.canonicalUrl) }
|
|
90
|
+
: {}),
|
|
91
|
+
...(postSendDelta === undefined ? {} : { postSendDelta })
|
|
92
|
+
});
|
|
93
|
+
const snapshot = Object.freeze({
|
|
94
|
+
schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
|
|
95
|
+
snapshotDigest,
|
|
96
|
+
target,
|
|
97
|
+
userTurns: Object.freeze(normalized.userTurns),
|
|
98
|
+
assistantTurns: Object.freeze(normalized.assistantTurns),
|
|
99
|
+
completeness: parsed.completeness,
|
|
100
|
+
terminalState: parsed.terminalState,
|
|
101
|
+
...(postSendDelta === undefined ? {} : { postSendDelta })
|
|
102
|
+
});
|
|
103
|
+
const terminalTurn = resolveTerminalTurn(parsed.turns, normalized.assistantTurns, options);
|
|
104
|
+
const terminal = terminalTurn === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: await terminalObservation(terminalTurn.raw, terminalTurn.normalized, normalized.userTurns, options, target);
|
|
107
|
+
const newTargetAnchor = options.target.targetLifecycle === "new_pending"
|
|
108
|
+
&& target.conversation.status === "unavailable"
|
|
109
|
+
&& target.canonicalThreadUrl.status === "unavailable"
|
|
110
|
+
&& normalized.userTurns.length === 0
|
|
111
|
+
&& normalized.assistantTurns.length === 0
|
|
112
|
+
? await (async () => {
|
|
113
|
+
const blankTaskEvidenceDigest = snapshot.snapshotDigest;
|
|
114
|
+
const anchorDigest = await digest(options.evidenceDigest, "browser-observation-new-target-anchor", {
|
|
115
|
+
operationId: options.operationId,
|
|
116
|
+
target: targetMaterial(target),
|
|
117
|
+
blankTaskEvidenceDigest
|
|
118
|
+
});
|
|
119
|
+
return Object.freeze({ anchorDigest, blankTaskEvidenceDigest });
|
|
120
|
+
})()
|
|
121
|
+
: undefined;
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
snapshot,
|
|
124
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
125
|
+
...(newTargetAnchor === undefined ? {} : { newTargetAnchor })
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* This function is intentionally self-contained because it is serialized into
|
|
130
|
+
* the browser evaluation boundary. It returns bounded, transient raw material;
|
|
131
|
+
* the outer adapter converts ownership material to HMAC evidence and never
|
|
132
|
+
* stores prompt/response text in the normalized snapshot. Exact response text
|
|
133
|
+
* is exposed only in the request-scoped terminal result when explicitly asked.
|
|
134
|
+
*/
|
|
135
|
+
export function readPageObservation(args) {
|
|
136
|
+
// IMPORTANT: page.evaluate serializes this function. Every runtime value it
|
|
137
|
+
// uses must therefore be declared inside this body; imported/module helpers
|
|
138
|
+
// are not part of the browser realm. Keep the outer twins below for SDK-side
|
|
139
|
+
// validation only.
|
|
140
|
+
const MAX_TURNS = 256;
|
|
141
|
+
const MAX_ARTIFACTS_PER_TURN = 32;
|
|
142
|
+
const MAX_TEXT_CHARS = 1_000_000;
|
|
143
|
+
const MAX_TOTAL_TEXT_CHARS = 8_000_000;
|
|
144
|
+
const MAX_NODES = 32_768;
|
|
145
|
+
const MAX_ATTRIBUTE_LENGTH = 4096;
|
|
146
|
+
const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
|
|
147
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
|
|
148
|
+
const boundedPositive = (value, fallback) => Number.isSafeInteger(value) && value > 0
|
|
149
|
+
? Math.min(value, fallback)
|
|
150
|
+
: fallback;
|
|
151
|
+
const isBoundedId = (value) => typeof value === "string" && ID_PATTERN.test(value);
|
|
152
|
+
const isBoundedText = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\u0000");
|
|
153
|
+
const isContentDigest = (value) => typeof value === "string" && /^(?:hmac-sha256:|sha256:)[0-9a-f]{64}$/.test(value);
|
|
154
|
+
const canonicalizeUrl = (value) => {
|
|
155
|
+
const url = new URL(value);
|
|
156
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") {
|
|
157
|
+
throw new Error("non-canonical navigation");
|
|
158
|
+
}
|
|
159
|
+
url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/u, "") || "/";
|
|
160
|
+
return url.toString();
|
|
161
|
+
};
|
|
162
|
+
const conversationIdFromUrl = (urlValue) => {
|
|
163
|
+
const url = new URL(urlValue);
|
|
164
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
165
|
+
const index = parts.findIndex(part => part === "c" || part === "conversation");
|
|
166
|
+
const candidate = index >= 0 ? parts[index + 1] : undefined;
|
|
167
|
+
return candidate !== undefined && ID_PATTERN.test(candidate) ? candidate : undefined;
|
|
168
|
+
};
|
|
169
|
+
const uniqueNodeAttribute = (root, roleNodes, names) => {
|
|
170
|
+
const values = new Set();
|
|
171
|
+
for (const node of [root, ...roleNodes]) {
|
|
172
|
+
for (const name of names) {
|
|
173
|
+
const value = node.getAttribute(name);
|
|
174
|
+
if (value !== null && value.length > 0) {
|
|
175
|
+
if (value.length > MAX_ATTRIBUTE_LENGTH || !ID_PATTERN.test(value))
|
|
176
|
+
throw new Error("attribute drift");
|
|
177
|
+
values.add(value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (values.size > 1)
|
|
182
|
+
throw new Error("unstable identity");
|
|
183
|
+
return [...values][0];
|
|
184
|
+
};
|
|
185
|
+
const preferredNodeAttribute = (root, roleNodes, names) => {
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
const values = new Set();
|
|
188
|
+
for (const node of [root, ...roleNodes]) {
|
|
189
|
+
const value = node.getAttribute(name);
|
|
190
|
+
if (value !== null && value.length > 0) {
|
|
191
|
+
if (value.length > MAX_ATTRIBUTE_LENGTH || !ID_PATTERN.test(value))
|
|
192
|
+
throw new Error("attribute drift");
|
|
193
|
+
values.add(value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (values.size > 1)
|
|
197
|
+
throw new Error("unstable identity");
|
|
198
|
+
const selected = [...values][0];
|
|
199
|
+
if (selected !== undefined)
|
|
200
|
+
return selected;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
};
|
|
204
|
+
const boundedText = (value, max) => {
|
|
205
|
+
if (value.length > max || value.includes("\u0000"))
|
|
206
|
+
throw new Error("text limit exceeded");
|
|
207
|
+
return value;
|
|
208
|
+
};
|
|
209
|
+
const boundedMarkup = (value, max) => {
|
|
210
|
+
if (typeof value !== "string" || value.length > max || value.includes("\u0000"))
|
|
211
|
+
throw new Error("markup limit exceeded");
|
|
212
|
+
return value;
|
|
213
|
+
};
|
|
214
|
+
const readTurnState = (root, roleNode) => {
|
|
215
|
+
const values = [root, roleNode].flatMap(node => [
|
|
216
|
+
node.getAttribute("data-generation-state"),
|
|
217
|
+
node.getAttribute("data-state"),
|
|
218
|
+
node.getAttribute("data-status"),
|
|
219
|
+
node.getAttribute("data-turn-state"),
|
|
220
|
+
node.getAttribute("data-generating") === "true" ? "generating" : node.getAttribute("data-generating") === "false" ? "terminal" : null,
|
|
221
|
+
node.getAttribute("aria-busy") === "true" ? "generating" : node.getAttribute("aria-busy") === "false" ? "terminal" : null
|
|
222
|
+
]).filter((value) => value !== null && value !== undefined).map(value => value.trim().toLowerCase());
|
|
223
|
+
const generating = values.some(value => ["generating", "streaming", "in_progress", "in-progress", "true"].includes(value));
|
|
224
|
+
const terminal = values.some(value => ["terminal", "complete", "completed", "done", "false"].includes(value));
|
|
225
|
+
if (generating && terminal)
|
|
226
|
+
throw new Error("unstable generation state");
|
|
227
|
+
return generating ? "generating" : terminal ? "terminal" : undefined;
|
|
228
|
+
};
|
|
229
|
+
const readFinishReason = (root, roleNode) => {
|
|
230
|
+
for (const node of [root, roleNode]) {
|
|
231
|
+
for (const name of ["data-finish-reason", "data-stop-reason", "data-completion-reason"]) {
|
|
232
|
+
const value = node.getAttribute(name);
|
|
233
|
+
if (value !== null && value.length > 0)
|
|
234
|
+
return value.trim().slice(0, 128);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
};
|
|
239
|
+
const validateRawTurnOrdering = (turns) => {
|
|
240
|
+
const counters = { user: 0, assistant: 0 };
|
|
241
|
+
for (const turn of turns) {
|
|
242
|
+
if (turn.ordinal !== counters[turn.role])
|
|
243
|
+
throw new Error("out-of-order turn");
|
|
244
|
+
counters[turn.role] += 1;
|
|
245
|
+
}
|
|
246
|
+
const users = new Set(turns.filter(turn => turn.role === "user").map(turn => turn.stableId));
|
|
247
|
+
for (const turn of turns) {
|
|
248
|
+
if (turn.role === "assistant" && (turn.parentStableId === undefined || !users.has(turn.parentStableId))) {
|
|
249
|
+
throw new Error("branch ambiguity");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const maxTurns = boundedPositive(args?.maxTurns, MAX_TURNS);
|
|
254
|
+
const maxTextChars = boundedPositive(args?.maxTextChars, MAX_TEXT_CHARS);
|
|
255
|
+
const maxArtifactsPerTurn = boundedPositive(args?.maxArtifactsPerTurn, MAX_ARTIFACTS_PER_TURN);
|
|
256
|
+
const captureAssistantTurnId = args?.captureAssistantTurnId;
|
|
257
|
+
if (captureAssistantTurnId !== undefined && !isBoundedId(captureAssistantTurnId))
|
|
258
|
+
throw new Error("capture identity unavailable");
|
|
259
|
+
const allowBlankTask = args?.allowBlankTask === true;
|
|
260
|
+
// Chrome's serialized evaluation realm exposes the ambient DOM globals but
|
|
261
|
+
// can intentionally shadow `globalThis`. Use guarded ambient bindings so
|
|
262
|
+
// this self-contained callback works in that realm and still fails closed
|
|
263
|
+
// when reconstructed outside a browser.
|
|
264
|
+
const documentRoot = typeof document === "undefined" ? undefined : document;
|
|
265
|
+
if (documentRoot === undefined)
|
|
266
|
+
throw new Error("document unavailable");
|
|
267
|
+
const locationRoot = typeof location === "undefined" ? undefined : location;
|
|
268
|
+
const currentUrl = locationRoot?.href;
|
|
269
|
+
if (typeof currentUrl !== "string" || currentUrl.length === 0 || currentUrl.length > MAX_ATTRIBUTE_LENGTH) {
|
|
270
|
+
throw new Error("navigation unavailable");
|
|
271
|
+
}
|
|
272
|
+
const canonicalUrl = canonicalizeUrl(currentUrl);
|
|
273
|
+
const fromUrl = conversationIdFromUrl(canonicalUrl);
|
|
274
|
+
const conversationValues = new Set();
|
|
275
|
+
const threadValues = new Set();
|
|
276
|
+
const roots = [];
|
|
277
|
+
const activeArtifacts = [];
|
|
278
|
+
const activeElements = [];
|
|
279
|
+
let roleNodeCount = 0;
|
|
280
|
+
let blankTaskMarker = false;
|
|
281
|
+
let visibleComposerCount = 0;
|
|
282
|
+
let visibleStructuralStopControlCount = 0;
|
|
283
|
+
let completenessMarker;
|
|
284
|
+
let visitedNodes = 0;
|
|
285
|
+
const consumeNode = () => {
|
|
286
|
+
visitedNodes += 1;
|
|
287
|
+
if (visitedNodes > MAX_NODES)
|
|
288
|
+
throw new Error("node limit exceeded");
|
|
289
|
+
};
|
|
290
|
+
const addIdentityValues = (element, names, values) => {
|
|
291
|
+
for (const name of names) {
|
|
292
|
+
const value = element.getAttribute(name);
|
|
293
|
+
if (value !== null && value.length > 0) {
|
|
294
|
+
if (value.length > maxTextChars || !ID_PATTERN.test(value))
|
|
295
|
+
throw new Error("attribute drift");
|
|
296
|
+
values.add(value);
|
|
297
|
+
if (values.size > 1)
|
|
298
|
+
throw new Error("ambiguous identity");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const isStructuralTurnRoot = (element) => {
|
|
303
|
+
const testId = element.getAttribute("data-testid");
|
|
304
|
+
return (testId !== null && testId.startsWith("conversation-turn"))
|
|
305
|
+
|| element.hasAttribute("data-conversation-turn-id")
|
|
306
|
+
|| element.hasAttribute("data-turn-id");
|
|
307
|
+
};
|
|
308
|
+
const isFallbackTurnRoot = (element) => element.hasAttribute("data-message-id")
|
|
309
|
+
&& element.hasAttribute("data-message-author-role");
|
|
310
|
+
const isRendered = (element) => {
|
|
311
|
+
const style = typeof getComputedStyle === "function" ? getComputedStyle(element) : undefined;
|
|
312
|
+
if (style !== undefined && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0"))
|
|
313
|
+
return false;
|
|
314
|
+
const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : undefined;
|
|
315
|
+
return rect === undefined || rect.width > 0 || rect.height > 0;
|
|
316
|
+
};
|
|
317
|
+
const isStructuralStopControl = (element) => element.tagName.toLowerCase() === "button"
|
|
318
|
+
&& (element.getAttribute("data-testid") === "stop-button" || element.getAttribute("id") === "composer-stop-button");
|
|
319
|
+
const isIgnoredMessageElement = (element) => {
|
|
320
|
+
const tag = element.tagName.toLowerCase();
|
|
321
|
+
return tag === "button"
|
|
322
|
+
|| tag === "script"
|
|
323
|
+
|| tag === "style"
|
|
324
|
+
|| tag === "svg"
|
|
325
|
+
|| element.getAttribute("role") === "button"
|
|
326
|
+
|| element.getAttribute("aria-hidden") === "true";
|
|
327
|
+
};
|
|
328
|
+
const isArtifact = (element) => {
|
|
329
|
+
const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
|
|
330
|
+
const tag = element.tagName.toLowerCase();
|
|
331
|
+
return element.hasAttribute("data-artifact-id")
|
|
332
|
+
|| element.hasAttribute("data-file-id")
|
|
333
|
+
|| element.hasAttribute("data-attachment-id")
|
|
334
|
+
|| element.hasAttribute("data-image-id")
|
|
335
|
+
|| testId.includes("artifact")
|
|
336
|
+
|| testId.includes("file")
|
|
337
|
+
|| testId.includes("image")
|
|
338
|
+
|| (tag === "a" && element.hasAttribute("download"));
|
|
339
|
+
};
|
|
340
|
+
const isBlankTaskMarker = (element) => {
|
|
341
|
+
const hasMarker = element.hasAttribute("data-new-task")
|
|
342
|
+
|| element.hasAttribute("data-new-conversation")
|
|
343
|
+
|| ((element.getAttribute("data-testid") ?? "").toLowerCase().includes("new-chat"))
|
|
344
|
+
|| ((element.getAttribute("data-testid") ?? "").toLowerCase().includes("new-conversation"));
|
|
345
|
+
if (!hasMarker)
|
|
346
|
+
return false;
|
|
347
|
+
const explicit = element.getAttribute("data-new-task") ?? element.getAttribute("data-new-conversation");
|
|
348
|
+
if (explicit === null || explicit === "" || explicit.toLowerCase() === "true")
|
|
349
|
+
return true;
|
|
350
|
+
const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
|
|
351
|
+
return testId.includes("new-chat") || testId.includes("new-conversation");
|
|
352
|
+
};
|
|
353
|
+
const isComposer = (element) => {
|
|
354
|
+
const tag = element.tagName.toLowerCase();
|
|
355
|
+
return tag === "textarea"
|
|
356
|
+
|| element.getAttribute("contenteditable") === "true"
|
|
357
|
+
|| element.getAttribute("role") === "textbox";
|
|
358
|
+
};
|
|
359
|
+
const readArtifact = (element, turn) => {
|
|
360
|
+
const identityValues = new Set();
|
|
361
|
+
for (const name of ["data-artifact-id", "data-file-id", "data-attachment-id", "data-image-id"]) {
|
|
362
|
+
const value = element.getAttribute(name);
|
|
363
|
+
if (value !== null && value.length > 0) {
|
|
364
|
+
if (!isBoundedId(value))
|
|
365
|
+
throw new Error("artifact identity unavailable");
|
|
366
|
+
identityValues.add(value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (identityValues.size > 1)
|
|
370
|
+
throw new Error("artifact identity unavailable");
|
|
371
|
+
const identity = [...identityValues][0] ?? (element.getAttribute("data-testid") ?? "");
|
|
372
|
+
if (!isBoundedId(identity) || turn.artifactIds.has(identity))
|
|
373
|
+
throw new Error("artifact identity unavailable");
|
|
374
|
+
turn.artifactIds.add(identity);
|
|
375
|
+
if (turn.artifacts.length >= maxArtifactsPerTurn)
|
|
376
|
+
throw new Error("artifact limit exceeded");
|
|
377
|
+
const testId = (element.getAttribute("data-testid") ?? "").toLowerCase();
|
|
378
|
+
const tag = element.tagName.toLowerCase();
|
|
379
|
+
const kind = tag === "img" || testId.includes("image") || element.hasAttribute("data-image-id")
|
|
380
|
+
? "image"
|
|
381
|
+
: testId.includes("file") || element.hasAttribute("data-file-id") || element.hasAttribute("data-attachment-id") || tag === "a"
|
|
382
|
+
? "file"
|
|
383
|
+
: "other";
|
|
384
|
+
const contentDigest = element.getAttribute("data-content-sha256") ?? element.getAttribute("data-sha256") ?? undefined;
|
|
385
|
+
if (contentDigest !== undefined && !isContentDigest(contentDigest))
|
|
386
|
+
throw new Error("artifact digest drift");
|
|
387
|
+
const bytesRaw = element.getAttribute("data-bytes") ?? element.getAttribute("data-size") ?? undefined;
|
|
388
|
+
const bytes = bytesRaw === undefined ? undefined : Number(bytesRaw);
|
|
389
|
+
if (bytes !== undefined && (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > MAX_ARTIFACT_BYTES))
|
|
390
|
+
throw new Error("artifact bytes limit");
|
|
391
|
+
const mimeType = element.getAttribute("data-mime-type") ?? element.getAttribute("type") ?? undefined;
|
|
392
|
+
if (mimeType !== undefined && !isBoundedText(mimeType, 128))
|
|
393
|
+
throw new Error("artifact MIME drift");
|
|
394
|
+
const raw = {
|
|
395
|
+
kind,
|
|
396
|
+
identity,
|
|
397
|
+
...(contentDigest === undefined ? {} : { contentDigest }),
|
|
398
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
399
|
+
...(mimeType === undefined ? {} : { mimeType })
|
|
400
|
+
};
|
|
401
|
+
turn.artifacts.push(raw);
|
|
402
|
+
const state = { raw, textChars: 0 };
|
|
403
|
+
activeArtifacts.push(state);
|
|
404
|
+
return state;
|
|
405
|
+
};
|
|
406
|
+
// The Chrome bridge exposes firstChild/nextSibling but intentionally omits
|
|
407
|
+
// TreeWalker and NodeFilter. Walk depth-first with bounded DOM pointers so
|
|
408
|
+
// every element, text node, and comment still consumes exactly one slot.
|
|
409
|
+
const nextDepthFirstNode = (node, root) => {
|
|
410
|
+
if (node.firstChild !== null)
|
|
411
|
+
return node.firstChild;
|
|
412
|
+
let cursor = node;
|
|
413
|
+
while (cursor !== null && cursor !== root) {
|
|
414
|
+
if (cursor.nextSibling !== null)
|
|
415
|
+
return cursor.nextSibling;
|
|
416
|
+
cursor = cursor.parentNode;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
const mainRoot = typeof documentRoot.getElementById === "function" ? documentRoot.getElementById("main") : null;
|
|
421
|
+
const traversalRoot = mainRoot !== null && mainRoot.tagName.toLowerCase() === "main" ? mainRoot : documentRoot;
|
|
422
|
+
let current = traversalRoot.firstChild;
|
|
423
|
+
while (current !== null) {
|
|
424
|
+
consumeNode();
|
|
425
|
+
while (activeElements.length > 0
|
|
426
|
+
&& activeElements[activeElements.length - 1]?.element !== current.parentNode) {
|
|
427
|
+
const closed = activeElements.pop();
|
|
428
|
+
if (closed?.artifact !== undefined) {
|
|
429
|
+
const popped = activeArtifacts.pop();
|
|
430
|
+
if (popped !== closed.artifact)
|
|
431
|
+
throw new Error("artifact traversal drift");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (current.nodeType === 1) {
|
|
435
|
+
const element = current;
|
|
436
|
+
const parent = activeElements[activeElements.length - 1];
|
|
437
|
+
const inheritedTurn = parent?.turn;
|
|
438
|
+
const inheritedHidden = parent?.hiddenAncestor ?? false;
|
|
439
|
+
const ownHidden = element.hidden === true
|
|
440
|
+
|| element.hasAttribute("hidden")
|
|
441
|
+
|| element.hasAttribute("inert")
|
|
442
|
+
|| element.getAttribute("aria-hidden") === "true";
|
|
443
|
+
const hiddenAncestor = inheritedHidden || ownHidden;
|
|
444
|
+
const role = element.getAttribute("data-message-author-role");
|
|
445
|
+
const structuralTurnCandidate = isStructuralTurnRoot(element);
|
|
446
|
+
const fallbackTurnCandidate = inheritedTurn === undefined && isFallbackTurnRoot(element);
|
|
447
|
+
const identityCandidate = element.hasAttribute("data-conversation-id")
|
|
448
|
+
|| element.hasAttribute("data-chat-id")
|
|
449
|
+
|| element.hasAttribute("data-thread-id")
|
|
450
|
+
|| element.hasAttribute("data-conversation-thread-id");
|
|
451
|
+
const completenessCandidate = element.hasAttribute("data-observation-completeness")
|
|
452
|
+
|| element.hasAttribute("data-conversation-completeness")
|
|
453
|
+
|| element.hasAttribute("data-turns-truncated");
|
|
454
|
+
const blankTaskCandidate = isBlankTaskMarker(element);
|
|
455
|
+
const composerCandidate = isComposer(element);
|
|
456
|
+
const stopControlCandidate = isStructuralStopControl(element);
|
|
457
|
+
const artifactCandidate = inheritedTurn !== undefined && isArtifact(element);
|
|
458
|
+
// Layout/style reads are the expensive part of the aggregate DOM probe.
|
|
459
|
+
// Only nodes that can affect a returned semantic fact need one; ordinary
|
|
460
|
+
// message wrappers and text descendants remain covered by the same node
|
|
461
|
+
// and hidden-ancestor budgets without forcing layout per element.
|
|
462
|
+
const needsRenderedCheck = structuralTurnCandidate
|
|
463
|
+
|| fallbackTurnCandidate
|
|
464
|
+
|| role !== null
|
|
465
|
+
|| identityCandidate
|
|
466
|
+
|| completenessCandidate
|
|
467
|
+
|| blankTaskCandidate
|
|
468
|
+
|| composerCandidate
|
|
469
|
+
|| stopControlCandidate
|
|
470
|
+
|| artifactCandidate;
|
|
471
|
+
const semanticVisible = !hiddenAncestor && (!needsRenderedCheck || isRendered(element));
|
|
472
|
+
const structuralRoot = semanticVisible && structuralTurnCandidate;
|
|
473
|
+
const rootCandidate = structuralRoot || (semanticVisible && fallbackTurnCandidate);
|
|
474
|
+
// A nested provider root would otherwise make one role/artifact subtree
|
|
475
|
+
// belong to two owners. Reject it before descending so the exact visit
|
|
476
|
+
// budget cannot be multiplied by overlapping ownership interpretations.
|
|
477
|
+
if (structuralRoot && inheritedTurn !== undefined)
|
|
478
|
+
throw new Error("nested turn root");
|
|
479
|
+
let turn = rootCandidate
|
|
480
|
+
? {
|
|
481
|
+
root: element,
|
|
482
|
+
roleMarkers: [],
|
|
483
|
+
directChildCount: 0,
|
|
484
|
+
artifacts: [],
|
|
485
|
+
artifactIds: new Set(),
|
|
486
|
+
messageChunks: [],
|
|
487
|
+
messageChars: 0
|
|
488
|
+
}
|
|
489
|
+
: inheritedTurn;
|
|
490
|
+
if (rootCandidate) {
|
|
491
|
+
if (turn === undefined)
|
|
492
|
+
throw new Error("turn traversal drift");
|
|
493
|
+
roots.push(turn);
|
|
494
|
+
}
|
|
495
|
+
if (parent !== undefined && parent.turn?.root === parent.element)
|
|
496
|
+
parent.turn.directChildCount += 1;
|
|
497
|
+
if (role !== null && semanticVisible) {
|
|
498
|
+
roleNodeCount += 1;
|
|
499
|
+
if (role !== "user" && role !== "assistant")
|
|
500
|
+
throw new Error("role drift");
|
|
501
|
+
if (turn === undefined) {
|
|
502
|
+
turn = {
|
|
503
|
+
root: element,
|
|
504
|
+
roleMarkers: [],
|
|
505
|
+
directChildCount: 0,
|
|
506
|
+
artifacts: [],
|
|
507
|
+
artifactIds: new Set(),
|
|
508
|
+
messageChunks: [],
|
|
509
|
+
messageChars: 0
|
|
510
|
+
};
|
|
511
|
+
roots.push(turn);
|
|
512
|
+
}
|
|
513
|
+
turn.roleMarkers.push(element);
|
|
514
|
+
if (turn.firstRoleNode === undefined)
|
|
515
|
+
turn.firstRoleNode = element;
|
|
516
|
+
}
|
|
517
|
+
if (semanticVisible)
|
|
518
|
+
addIdentityValues(element, ["data-conversation-id", "data-chat-id"], conversationValues);
|
|
519
|
+
if (semanticVisible)
|
|
520
|
+
addIdentityValues(element, ["data-thread-id", "data-conversation-thread-id"], threadValues);
|
|
521
|
+
if (semanticVisible && completenessMarker === undefined && completenessCandidate) {
|
|
522
|
+
completenessMarker = (element.getAttribute("data-observation-completeness")
|
|
523
|
+
?? element.getAttribute("data-conversation-completeness")
|
|
524
|
+
?? element.getAttribute("data-turns-truncated")
|
|
525
|
+
?? "").toLowerCase();
|
|
526
|
+
}
|
|
527
|
+
if (semanticVisible && blankTaskCandidate)
|
|
528
|
+
blankTaskMarker = true;
|
|
529
|
+
if (semanticVisible && composerCandidate)
|
|
530
|
+
visibleComposerCount += 1;
|
|
531
|
+
if (semanticVisible && stopControlCandidate)
|
|
532
|
+
visibleStructuralStopControlCount += 1;
|
|
533
|
+
const selectedRoleNode = turn?.firstRoleNode === element;
|
|
534
|
+
const messageOwner = selectedRoleNode ? turn : parent?.messageOwner;
|
|
535
|
+
const messageIgnored = selectedRoleNode
|
|
536
|
+
? false
|
|
537
|
+
: (parent?.messageIgnored ?? false) || hiddenAncestor || isIgnoredMessageElement(element);
|
|
538
|
+
let artifact;
|
|
539
|
+
if (semanticVisible && turn !== undefined && turn.root !== element && artifactCandidate)
|
|
540
|
+
artifact = readArtifact(element, turn);
|
|
541
|
+
activeElements.push({
|
|
542
|
+
element,
|
|
543
|
+
...(turn === undefined ? {} : { turn }),
|
|
544
|
+
...(messageOwner === undefined ? {} : { messageOwner }),
|
|
545
|
+
messageIgnored,
|
|
546
|
+
...(artifact === undefined ? {} : { artifact }),
|
|
547
|
+
hiddenAncestor
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
else if (current.nodeType === 3) {
|
|
551
|
+
const value = current.nodeValue ?? "";
|
|
552
|
+
for (const artifact of activeArtifacts) {
|
|
553
|
+
artifact.textChars += value.length;
|
|
554
|
+
if (artifact.textChars > maxTextChars)
|
|
555
|
+
throw new Error("artifact text limit");
|
|
556
|
+
}
|
|
557
|
+
const owner = activeElements[activeElements.length - 1];
|
|
558
|
+
if (owner?.messageOwner !== undefined && !owner.messageIgnored) {
|
|
559
|
+
owner.messageOwner.messageChars += value.length;
|
|
560
|
+
if (owner.messageOwner.messageChars > maxTextChars)
|
|
561
|
+
throw new Error("text limit exceeded");
|
|
562
|
+
if (value.length > 0)
|
|
563
|
+
owner.messageOwner.messageChunks.push(value);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
current = nextDepthFirstNode(current, traversalRoot);
|
|
567
|
+
}
|
|
568
|
+
while (activeElements.length > 0) {
|
|
569
|
+
const closed = activeElements.pop();
|
|
570
|
+
if (closed?.artifact !== undefined)
|
|
571
|
+
activeArtifacts.pop();
|
|
572
|
+
}
|
|
573
|
+
const conversationId = [...conversationValues][0];
|
|
574
|
+
const threadId = [...threadValues][0];
|
|
575
|
+
// A stale rendered conversation must never override a different current
|
|
576
|
+
// navigation target. Both sources are independently useful, but when both
|
|
577
|
+
// are exposed they must identify the same provider conversation exactly.
|
|
578
|
+
if (conversationId !== undefined && fromUrl !== undefined && conversationId !== fromUrl) {
|
|
579
|
+
throw new Error("conversation navigation mismatch");
|
|
580
|
+
}
|
|
581
|
+
const resolvedConversationId = conversationId ?? fromUrl;
|
|
582
|
+
const resolvedThreadId = threadId ?? resolvedConversationId;
|
|
583
|
+
if ((resolvedConversationId === undefined || resolvedThreadId === undefined)
|
|
584
|
+
&& (!allowBlankTask || roleNodeCount !== 0 || (!blankTaskMarker && visibleComposerCount !== 1))) {
|
|
585
|
+
throw new Error("conversation identity unavailable");
|
|
586
|
+
}
|
|
587
|
+
const outputRoots = roots.filter(root => root.roleMarkers.length > 0);
|
|
588
|
+
if (outputRoots.length > maxTurns)
|
|
589
|
+
throw new Error("turn limit exceeded");
|
|
590
|
+
let totalTextChars = 0;
|
|
591
|
+
let turns = [];
|
|
592
|
+
for (const root of outputRoots) {
|
|
593
|
+
const roleMarkers = root.roleMarkers;
|
|
594
|
+
const distinctRoles = new Set(roleMarkers.map(node => node.getAttribute("data-message-author-role")));
|
|
595
|
+
if (distinctRoles.size !== 1)
|
|
596
|
+
throw new Error("role ambiguity");
|
|
597
|
+
const role = [...distinctRoles][0];
|
|
598
|
+
if (role !== "user" && role !== "assistant")
|
|
599
|
+
throw new Error("role drift");
|
|
600
|
+
const roleNode = roleMarkers[0];
|
|
601
|
+
if (roleNode === undefined)
|
|
602
|
+
throw new Error("role missing");
|
|
603
|
+
const stableId = preferredNodeAttribute(root.root, roleMarkers, ["data-message-id", "data-conversation-turn-id", "data-turn-id"]);
|
|
604
|
+
if (stableId === undefined)
|
|
605
|
+
throw new Error("turn identity unavailable");
|
|
606
|
+
const explicitParentStableId = uniqueNodeAttribute(root.root, roleMarkers, ["data-parent-message-id", "data-parent-turn-id", "data-parent-id"]);
|
|
607
|
+
const explicitBranchStableId = uniqueNodeAttribute(root.root, roleMarkers, ["data-branch-id", "data-conversation-branch-id", "data-message-branch-id"]);
|
|
608
|
+
const previousTurn = turns.at(-1);
|
|
609
|
+
const parentStableId = role === "assistant"
|
|
610
|
+
? explicitParentStableId ?? (previousTurn?.role === "user" ? previousTurn.stableId : undefined)
|
|
611
|
+
: explicitParentStableId;
|
|
612
|
+
if (role === "assistant" && parentStableId === undefined)
|
|
613
|
+
throw new Error("branch ambiguity");
|
|
614
|
+
const branchStableId = role === "assistant" ? explicitBranchStableId ?? stableId : explicitBranchStableId;
|
|
615
|
+
const text = boundedText(root.messageChunks.join("").replace(/\s+/g, " ").trim().normalize("NFC"), maxTextChars);
|
|
616
|
+
totalTextChars += text.length;
|
|
617
|
+
if (totalTextChars > MAX_TOTAL_TEXT_CHARS)
|
|
618
|
+
throw new Error("text limit exceeded");
|
|
619
|
+
const artifacts = root.artifacts;
|
|
620
|
+
const state = readTurnState(root.root, roleNode);
|
|
621
|
+
const finishReason = readFinishReason(root.root, roleNode);
|
|
622
|
+
// Only read innerHTML for the exact assistant ID requested by the outer
|
|
623
|
+
// transaction. This keeps transient markup bounded and prevents a
|
|
624
|
+
// page-wide/latest-message fallback from entering the response path.
|
|
625
|
+
const contentHtml = role === "assistant" && captureAssistantTurnId === stableId
|
|
626
|
+
? boundedMarkup(roleNode.innerHTML, maxTextChars)
|
|
627
|
+
: undefined;
|
|
628
|
+
turns.push({
|
|
629
|
+
role,
|
|
630
|
+
stableId,
|
|
631
|
+
...(parentStableId === undefined ? {} : { parentStableId }),
|
|
632
|
+
...(branchStableId === undefined ? {} : { branchStableId }),
|
|
633
|
+
ordinal: role === "user" ? turns.filter(turn => turn.role === "user").length : turns.filter(turn => turn.role === "assistant").length,
|
|
634
|
+
text,
|
|
635
|
+
...(contentHtml === undefined ? {} : { contentHtml }),
|
|
636
|
+
structure: {
|
|
637
|
+
tag: root.root.tagName.toLowerCase().slice(0, 64),
|
|
638
|
+
childCount: Math.min(root.directChildCount, MAX_NODES),
|
|
639
|
+
artifactCount: artifacts.length
|
|
640
|
+
},
|
|
641
|
+
...(state === undefined ? {} : { state }),
|
|
642
|
+
...(finishReason === undefined ? {} : { finishReason }),
|
|
643
|
+
artifacts
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (visibleStructuralStopControlCount > 1)
|
|
647
|
+
throw new Error("generation state ambiguity");
|
|
648
|
+
const assistantIndexes = turns.flatMap((turn, index) => turn.role === "assistant" ? [index] : []);
|
|
649
|
+
if (visibleStructuralStopControlCount === 1 && assistantIndexes.length === 0)
|
|
650
|
+
throw new Error("assistant identity unavailable");
|
|
651
|
+
const latestAssistantIndex = assistantIndexes.at(-1);
|
|
652
|
+
turns = turns.map((turn, index) => {
|
|
653
|
+
if (turn.role !== "assistant")
|
|
654
|
+
return turn;
|
|
655
|
+
const inferredState = visibleStructuralStopControlCount === 1 && index === latestAssistantIndex
|
|
656
|
+
? "generating"
|
|
657
|
+
: "terminal";
|
|
658
|
+
if (turn.state !== undefined && turn.state !== inferredState)
|
|
659
|
+
throw new Error("unstable generation state");
|
|
660
|
+
return {
|
|
661
|
+
...turn,
|
|
662
|
+
state: turn.state ?? inferredState,
|
|
663
|
+
...((turn.state ?? inferredState) === "terminal" && turn.finishReason === undefined
|
|
664
|
+
? { finishReason: "provider_terminal" }
|
|
665
|
+
: {})
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
validateRawTurnOrdering(turns);
|
|
669
|
+
const assistantTurns = turns.filter(turn => turn.role === "assistant");
|
|
670
|
+
if (assistantTurns.some(turn => turn.state === undefined))
|
|
671
|
+
throw new Error("assistant state unavailable");
|
|
672
|
+
const generating = assistantTurns.some(turn => turn.state === "generating");
|
|
673
|
+
const terminal = assistantTurns.length > 0 && assistantTurns.every(turn => turn.state === "terminal");
|
|
674
|
+
const terminalState = generating
|
|
675
|
+
? "generating"
|
|
676
|
+
: terminal
|
|
677
|
+
? "terminal"
|
|
678
|
+
: assistantTurns.length === 0
|
|
679
|
+
? "idle"
|
|
680
|
+
: "unknown";
|
|
681
|
+
const completeness = completenessMarker === "truncated" || completenessMarker === "true"
|
|
682
|
+
? "truncated"
|
|
683
|
+
: completenessMarker === "incomplete"
|
|
684
|
+
? "incomplete"
|
|
685
|
+
: completenessMarker === "out_of_order"
|
|
686
|
+
? "out_of_order"
|
|
687
|
+
: "complete";
|
|
688
|
+
if (completeness !== "complete")
|
|
689
|
+
throw new Error("incomplete conversation DOM");
|
|
690
|
+
return {
|
|
691
|
+
canonicalUrl,
|
|
692
|
+
...(resolvedConversationId === undefined ? {} : { conversationId: resolvedConversationId }),
|
|
693
|
+
...(resolvedThreadId === undefined ? {} : { threadId: resolvedThreadId }),
|
|
694
|
+
turns,
|
|
695
|
+
completeness,
|
|
696
|
+
terminalState
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function validateOptions(options) {
|
|
700
|
+
if (!isRecord(options) || !isBoundedId(options.operationId))
|
|
701
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
702
|
+
if (typeof options.evidenceDigest !== "function")
|
|
703
|
+
throw new BrowserObservationError("evidence_digest_failed");
|
|
704
|
+
if (options.responseContent !== undefined && options.responseContent !== "include" && options.responseContent !== "metadata") {
|
|
705
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
706
|
+
}
|
|
707
|
+
if (options.responseFormat !== undefined && options.responseFormat !== "markdown" && options.responseFormat !== "text") {
|
|
708
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
709
|
+
}
|
|
710
|
+
const target = options.target;
|
|
711
|
+
if (!isRecord(target)
|
|
712
|
+
|| !isBoundedId(target.providerId)
|
|
713
|
+
|| !isBoundedId(target.browserId)
|
|
714
|
+
|| !isBoundedId(target.tabId)
|
|
715
|
+
|| (target.coordinationScope !== "process" && target.coordinationScope !== "provider")
|
|
716
|
+
|| (target.targetLifecycle !== undefined
|
|
717
|
+
&& target.targetLifecycle !== "fixed"
|
|
718
|
+
&& target.targetLifecycle !== "new_pending"
|
|
719
|
+
&& target.targetLifecycle !== "new_established")
|
|
720
|
+
|| (target.coordinationScope === "provider" && target.authoritativeTabClaim === undefined)
|
|
721
|
+
|| (target.authoritativeTabClaim !== undefined && !isBoundedId(target.authoritativeTabClaim))) {
|
|
722
|
+
throw new BrowserObservationError("missing_identity");
|
|
723
|
+
}
|
|
724
|
+
for (const value of [target.expectedConversationId, target.expectedThreadId, options.terminalAssistantTurnId, options.rawAssistantTurnId]) {
|
|
725
|
+
if (value !== undefined && !isBoundedId(value))
|
|
726
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
727
|
+
}
|
|
728
|
+
for (const value of [options.maxTurns, options.maxTextChars, options.maxArtifactsPerTurn]) {
|
|
729
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
|
|
730
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
731
|
+
}
|
|
732
|
+
if (options.baseline !== undefined && options.baseline.completeness !== "complete") {
|
|
733
|
+
throw new BrowserObservationError("incomplete_dom");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function parseRawObservation(value, args) {
|
|
737
|
+
if (!isRecord(value))
|
|
738
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
739
|
+
assertExactKeys(value, ["canonicalUrl", "conversationId", "threadId", "turns", "completeness", "terminalState"]);
|
|
740
|
+
if (typeof value.canonicalUrl !== "string") {
|
|
741
|
+
throw new BrowserObservationError("navigation_ambiguous");
|
|
742
|
+
}
|
|
743
|
+
if (!isBoundedUrl(value.canonicalUrl)
|
|
744
|
+
|| (value.conversationId !== undefined && !isBoundedId(value.conversationId))
|
|
745
|
+
|| (value.threadId !== undefined && !isBoundedId(value.threadId))) {
|
|
746
|
+
throw new BrowserObservationError("navigation_ambiguous");
|
|
747
|
+
}
|
|
748
|
+
if ((value.conversationId === undefined || value.threadId === undefined)
|
|
749
|
+
&& (!args.allowBlankTask || value.conversationId !== undefined || value.threadId !== undefined)) {
|
|
750
|
+
throw new BrowserObservationError("navigation_ambiguous");
|
|
751
|
+
}
|
|
752
|
+
if (!Array.isArray(value.turns) || value.turns.length > args.maxTurns)
|
|
753
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
754
|
+
if (value.completeness !== "complete" && value.completeness !== "truncated" && value.completeness !== "incomplete" && value.completeness !== "out_of_order") {
|
|
755
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
756
|
+
}
|
|
757
|
+
if (value.terminalState !== "idle" && value.terminalState !== "generating" && value.terminalState !== "terminal" && value.terminalState !== "unknown") {
|
|
758
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
759
|
+
}
|
|
760
|
+
const turns = [];
|
|
761
|
+
const seenIds = new Set();
|
|
762
|
+
let totalTextChars = 0;
|
|
763
|
+
for (const candidate of value.turns) {
|
|
764
|
+
if (!isRecord(candidate))
|
|
765
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
766
|
+
assertExactKeys(candidate, ["role", "stableId", "parentStableId", "branchStableId", "ordinal", "text", "contentHtml", "structure", "state", "finishReason", "artifacts"]);
|
|
767
|
+
if (candidate.role !== "user" && candidate.role !== "assistant")
|
|
768
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
769
|
+
if (!isBoundedId(candidate.stableId))
|
|
770
|
+
throw new BrowserObservationError("missing_identity");
|
|
771
|
+
if (seenIds.has(candidate.stableId))
|
|
772
|
+
throw new BrowserObservationError("duplicate_identity");
|
|
773
|
+
seenIds.add(candidate.stableId);
|
|
774
|
+
if (!Number.isSafeInteger(candidate.ordinal) || candidate.ordinal < 0 || candidate.ordinal >= args.maxTurns)
|
|
775
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
776
|
+
if (typeof candidate.text !== "string" || candidate.text.length > args.maxTextChars)
|
|
777
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
778
|
+
totalTextChars += candidate.text.length;
|
|
779
|
+
if (totalTextChars > MAX_TOTAL_TEXT_CHARS)
|
|
780
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
781
|
+
if (candidate.contentHtml !== undefined) {
|
|
782
|
+
// A serialized page probe is only allowed to return markup for the
|
|
783
|
+
// exact requested assistant. Reject unsolicited/page-wide markup
|
|
784
|
+
// rather than silently treating it as a valid capture.
|
|
785
|
+
if (candidate.role !== "assistant" || args.captureAssistantTurnId === undefined || candidate.stableId !== args.captureAssistantTurnId) {
|
|
786
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
787
|
+
}
|
|
788
|
+
if (typeof candidate.contentHtml !== "string" || candidate.contentHtml.length > args.maxTextChars || candidate.contentHtml.includes("\u0000")) {
|
|
789
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (candidate.parentStableId !== undefined && !isBoundedId(candidate.parentStableId))
|
|
793
|
+
throw new BrowserObservationError("unstable_identity");
|
|
794
|
+
if (candidate.branchStableId !== undefined && !isBoundedId(candidate.branchStableId))
|
|
795
|
+
throw new BrowserObservationError("unstable_identity");
|
|
796
|
+
if (candidate.state !== undefined && candidate.state !== "generating" && candidate.state !== "terminal")
|
|
797
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
798
|
+
if (candidate.role === "assistant" && (candidate.parentStableId === undefined || candidate.branchStableId === undefined))
|
|
799
|
+
throw new BrowserObservationError("branch_ambiguous");
|
|
800
|
+
if (candidate.role === "assistant" && candidate.state === undefined)
|
|
801
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
802
|
+
if (candidate.finishReason !== undefined && (!isBoundedText(candidate.finishReason, 128) || /[\u0000-\u001f\u007f]/u.test(candidate.finishReason)))
|
|
803
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
804
|
+
if (!isRecord(candidate.structure)
|
|
805
|
+
|| typeof candidate.structure.tag !== "string"
|
|
806
|
+
|| !Number.isSafeInteger(candidate.structure.childCount)
|
|
807
|
+
|| !Number.isSafeInteger(candidate.structure.artifactCount)
|
|
808
|
+
|| candidate.structure.childCount < 0
|
|
809
|
+
|| candidate.structure.childCount > MAX_NODES
|
|
810
|
+
|| candidate.structure.artifactCount < 0) {
|
|
811
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
812
|
+
}
|
|
813
|
+
assertExactKeys(candidate.structure, ["tag", "childCount", "artifactCount"]);
|
|
814
|
+
if (!Array.isArray(candidate.artifacts) || candidate.artifacts.length > args.maxArtifactsPerTurn)
|
|
815
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
816
|
+
const artifactIds = new Set();
|
|
817
|
+
const artifacts = [];
|
|
818
|
+
for (const rawArtifact of candidate.artifacts) {
|
|
819
|
+
if (!isRecord(rawArtifact)
|
|
820
|
+
|| (rawArtifact.kind !== "file" && rawArtifact.kind !== "image" && rawArtifact.kind !== "other")
|
|
821
|
+
|| !isBoundedId(rawArtifact.identity))
|
|
822
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
823
|
+
assertExactKeys(rawArtifact, ["kind", "identity", "contentDigest", "bytes", "mimeType"]);
|
|
824
|
+
if (artifactIds.has(rawArtifact.identity))
|
|
825
|
+
throw new BrowserObservationError("duplicate_identity");
|
|
826
|
+
artifactIds.add(rawArtifact.identity);
|
|
827
|
+
if (rawArtifact.contentDigest !== undefined && !isContentDigest(rawArtifact.contentDigest))
|
|
828
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
829
|
+
if (rawArtifact.bytes !== undefined && (!Number.isSafeInteger(rawArtifact.bytes) || rawArtifact.bytes < 0 || rawArtifact.bytes > MAX_ARTIFACT_BYTES))
|
|
830
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
831
|
+
if (rawArtifact.mimeType !== undefined && !isBoundedText(rawArtifact.mimeType, 128))
|
|
832
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
833
|
+
artifacts.push(Object.freeze({
|
|
834
|
+
kind: rawArtifact.kind,
|
|
835
|
+
identity: rawArtifact.identity,
|
|
836
|
+
...(rawArtifact.contentDigest === undefined ? {} : { contentDigest: rawArtifact.contentDigest }),
|
|
837
|
+
...(rawArtifact.bytes === undefined ? {} : { bytes: rawArtifact.bytes }),
|
|
838
|
+
...(rawArtifact.mimeType === undefined ? {} : { mimeType: rawArtifact.mimeType })
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
841
|
+
turns.push(Object.freeze({
|
|
842
|
+
role: candidate.role,
|
|
843
|
+
stableId: candidate.stableId,
|
|
844
|
+
...(candidate.parentStableId === undefined ? {} : { parentStableId: candidate.parentStableId }),
|
|
845
|
+
...(candidate.branchStableId === undefined ? {} : { branchStableId: candidate.branchStableId }),
|
|
846
|
+
ordinal: candidate.ordinal,
|
|
847
|
+
text: candidate.text,
|
|
848
|
+
...(candidate.contentHtml === undefined ? {} : { contentHtml: candidate.contentHtml }),
|
|
849
|
+
structure: Object.freeze({
|
|
850
|
+
tag: candidate.structure.tag,
|
|
851
|
+
childCount: candidate.structure.childCount,
|
|
852
|
+
artifactCount: candidate.structure.artifactCount
|
|
853
|
+
}),
|
|
854
|
+
...(candidate.state === undefined ? {} : { state: candidate.state }),
|
|
855
|
+
...(candidate.finishReason === undefined ? {} : { finishReason: candidate.finishReason }),
|
|
856
|
+
artifacts: Object.freeze(artifacts)
|
|
857
|
+
}));
|
|
858
|
+
}
|
|
859
|
+
validateRawTurnOrdering(turns);
|
|
860
|
+
const assistant = turns.filter(turn => turn.role === "assistant");
|
|
861
|
+
if (assistant.some(turn => turn.state === undefined))
|
|
862
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
863
|
+
const hasGeneratingAssistant = assistant.some(turn => turn.state === "generating");
|
|
864
|
+
const hasOnlyTerminalAssistants = assistant.length > 0 && assistant.every(turn => turn.state === "terminal");
|
|
865
|
+
const expectedTerminalState = hasGeneratingAssistant
|
|
866
|
+
? "generating"
|
|
867
|
+
: hasOnlyTerminalAssistants
|
|
868
|
+
? "terminal"
|
|
869
|
+
: assistant.length === 0
|
|
870
|
+
? "idle"
|
|
871
|
+
: "unknown";
|
|
872
|
+
if (value.terminalState !== expectedTerminalState)
|
|
873
|
+
throw new BrowserObservationError("unstable_identity");
|
|
874
|
+
if (value.completeness !== "complete")
|
|
875
|
+
throw new BrowserObservationError("incomplete_dom");
|
|
876
|
+
return Object.freeze({
|
|
877
|
+
canonicalUrl: value.canonicalUrl,
|
|
878
|
+
conversationId: value.conversationId,
|
|
879
|
+
threadId: value.threadId,
|
|
880
|
+
turns: Object.freeze(turns),
|
|
881
|
+
completeness: value.completeness,
|
|
882
|
+
terminalState: value.terminalState
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
async function normalizeTurns(turns, options) {
|
|
886
|
+
const artifacts = turns.map(raw => new Array(raw.artifacts.length));
|
|
887
|
+
const evidence = new Array(turns.length);
|
|
888
|
+
const structure = new Array(turns.length);
|
|
889
|
+
// Artifact digests feed both later identities. Run each dependency stage
|
|
890
|
+
// through four workers, with lazy jobs and deterministic result positions.
|
|
891
|
+
await runTurnDigestJobs((function* () {
|
|
892
|
+
for (const [index, raw] of turns.entries()) {
|
|
893
|
+
for (const [ordinal, artifact] of raw.artifacts.entries()) {
|
|
894
|
+
yield async () => {
|
|
895
|
+
artifacts[index][ordinal] = await digest(options.evidenceDigest, "browser-observation-artifact", {
|
|
896
|
+
operationId: options.operationId,
|
|
897
|
+
turnId: raw.stableId,
|
|
898
|
+
ordinal,
|
|
899
|
+
kind: artifact.kind,
|
|
900
|
+
identity: artifact.identity,
|
|
901
|
+
...(artifact.contentDigest === undefined ? {} : { contentDigest: artifact.contentDigest }),
|
|
902
|
+
...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
|
|
903
|
+
...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType })
|
|
904
|
+
});
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
})());
|
|
909
|
+
await runTurnDigestJobs((function* () {
|
|
910
|
+
for (const [index, raw] of turns.entries()) {
|
|
911
|
+
const artifactEvidenceDigests = artifacts[index];
|
|
912
|
+
yield async () => {
|
|
913
|
+
evidence[index] = await digest(options.evidenceDigest, "browser-observation-turn", {
|
|
914
|
+
operationId: options.operationId,
|
|
915
|
+
role: raw.role,
|
|
916
|
+
stableId: raw.stableId,
|
|
917
|
+
...(raw.parentStableId === undefined ? {} : { parentStableId: raw.parentStableId }),
|
|
918
|
+
...(raw.branchStableId === undefined ? {} : { branchStableId: raw.branchStableId }),
|
|
919
|
+
ordinal: raw.ordinal,
|
|
920
|
+
text: raw.text,
|
|
921
|
+
artifacts: artifactEvidenceDigests
|
|
922
|
+
});
|
|
923
|
+
};
|
|
924
|
+
yield async () => {
|
|
925
|
+
structure[index] = await digest(options.evidenceDigest, "browser-observation-structure", {
|
|
926
|
+
operationId: options.operationId,
|
|
927
|
+
role: raw.role,
|
|
928
|
+
stableId: raw.stableId,
|
|
929
|
+
ordinal: raw.ordinal,
|
|
930
|
+
structure: raw.structure,
|
|
931
|
+
artifacts: artifactEvidenceDigests
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
})());
|
|
936
|
+
const users = [];
|
|
937
|
+
const assistants = [];
|
|
938
|
+
const assistantParents = new Map();
|
|
939
|
+
for (const [index, raw] of turns.entries()) {
|
|
940
|
+
const turn = Object.freeze({
|
|
941
|
+
stableId: raw.stableId,
|
|
942
|
+
evidenceDigest: evidence[index],
|
|
943
|
+
structureDigest: structure[index],
|
|
944
|
+
ordinal: raw.ordinal,
|
|
945
|
+
...(raw.parentStableId === undefined ? {} : { parentStableId: raw.parentStableId }),
|
|
946
|
+
...(raw.branchStableId === undefined ? {} : { branchStableId: raw.branchStableId }),
|
|
947
|
+
...(raw.state === undefined ? {} : { state: raw.state }),
|
|
948
|
+
artifactEvidenceDigests: Object.freeze(artifacts[index])
|
|
949
|
+
});
|
|
950
|
+
if (raw.role === "user")
|
|
951
|
+
users.push(turn);
|
|
952
|
+
else {
|
|
953
|
+
assistants.push(turn);
|
|
954
|
+
const parent = raw.parentStableId;
|
|
955
|
+
const branches = assistantParents.get(parent) ?? new Set();
|
|
956
|
+
if (branches.has(raw.branchStableId))
|
|
957
|
+
throw new BrowserObservationError("branch_ambiguous");
|
|
958
|
+
branches.add(raw.branchStableId);
|
|
959
|
+
assistantParents.set(parent, branches);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return { userTurns: users, assistantTurns: assistants };
|
|
963
|
+
}
|
|
964
|
+
async function runTurnDigestJobs(jobs) {
|
|
965
|
+
const iterator = jobs[Symbol.iterator]();
|
|
966
|
+
let failed = false;
|
|
967
|
+
let failure;
|
|
968
|
+
const worker = async () => {
|
|
969
|
+
while (!failed) {
|
|
970
|
+
try {
|
|
971
|
+
const next = iterator.next();
|
|
972
|
+
if (next.done)
|
|
973
|
+
return;
|
|
974
|
+
await next.value();
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
if (!failed) {
|
|
978
|
+
failed = true;
|
|
979
|
+
failure = error;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
// Drain already-started digest calls before returning a failure. No queued
|
|
985
|
+
// jobs are admitted after the first failed digest and none outlive this pass.
|
|
986
|
+
await Promise.all(Array.from({ length: 4 }, worker));
|
|
987
|
+
if (failed)
|
|
988
|
+
throw failure;
|
|
989
|
+
}
|
|
990
|
+
function resolveTerminalTurn(rawTurns, assistantTurns, options) {
|
|
991
|
+
const requestedId = options.terminalAssistantTurnId ?? options.rawAssistantTurnId;
|
|
992
|
+
if (requestedId === undefined)
|
|
993
|
+
return undefined;
|
|
994
|
+
const raw = rawTurns.find(turn => turn.role === "assistant" && turn.stableId === requestedId);
|
|
995
|
+
const normalized = assistantTurns.find(turn => turn.stableId === requestedId);
|
|
996
|
+
if (raw === undefined || normalized === undefined || raw.state !== "terminal")
|
|
997
|
+
throw new BrowserObservationError("raw_content_unavailable");
|
|
998
|
+
if (raw.finishReason === undefined)
|
|
999
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1000
|
+
return { raw, normalized };
|
|
1001
|
+
}
|
|
1002
|
+
async function terminalObservation(raw, normalized, userTurns, options, _target) {
|
|
1003
|
+
if (raw.parentStableId === undefined || raw.branchStableId === undefined || raw.state !== "terminal" || raw.finishReason === undefined) {
|
|
1004
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1005
|
+
}
|
|
1006
|
+
const user = userTurns.find(turn => turn.stableId === raw.parentStableId);
|
|
1007
|
+
if (user === undefined)
|
|
1008
|
+
throw new BrowserObservationError("branch_ambiguous");
|
|
1009
|
+
if (raw.contentHtml === undefined)
|
|
1010
|
+
throw new BrowserObservationError("raw_content_unavailable");
|
|
1011
|
+
const requestedFormat = options.responseFormat ?? "markdown";
|
|
1012
|
+
const normalizedFormat = normalizeResponseFormat(requestedFormat);
|
|
1013
|
+
if (normalizedFormat !== "markdown" && normalizedFormat !== "normalized_text") {
|
|
1014
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1015
|
+
}
|
|
1016
|
+
const maxResponseChars = boundedPositive(options.maxTextChars, MAX_TEXT_CHARS);
|
|
1017
|
+
let formatted;
|
|
1018
|
+
try {
|
|
1019
|
+
formatted = formatMessageHtml(raw.contentHtml, requestedFormat, maxResponseChars);
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1023
|
+
}
|
|
1024
|
+
if (formatted.captureLimit?.clipped === true)
|
|
1025
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
1026
|
+
if (formatted.text.length === 0 && raw.text.length > 0)
|
|
1027
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1028
|
+
if (formatted.text.length > maxResponseChars || utf8Bytes(formatted.text) > MAX_RESPONSE_BYTES) {
|
|
1029
|
+
throw new BrowserObservationError("bounded_limit_exceeded");
|
|
1030
|
+
}
|
|
1031
|
+
const textDigest = await digest(options.evidenceDigest, "browser-observation-response", {
|
|
1032
|
+
operationId: options.operationId,
|
|
1033
|
+
assistantTurnId: raw.stableId,
|
|
1034
|
+
responseFormat: requestedFormat,
|
|
1035
|
+
text: formatted.text
|
|
1036
|
+
});
|
|
1037
|
+
const text = Object.freeze({
|
|
1038
|
+
digest: textDigest,
|
|
1039
|
+
bytes: utf8Bytes(formatted.text),
|
|
1040
|
+
chars: formatted.text.length
|
|
1041
|
+
});
|
|
1042
|
+
const artifacts = raw.artifacts.map((artifact, ordinal) => {
|
|
1043
|
+
const sourceIdentityDigest = normalized.artifactEvidenceDigests?.[ordinal];
|
|
1044
|
+
if (sourceIdentityDigest === undefined)
|
|
1045
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1046
|
+
return Object.freeze({
|
|
1047
|
+
kind: artifact.kind,
|
|
1048
|
+
ordinal,
|
|
1049
|
+
sourceIdentityDigest,
|
|
1050
|
+
...(artifact.contentDigest === undefined ? {} : { contentDigest: artifact.contentDigest }),
|
|
1051
|
+
...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
|
|
1052
|
+
...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType })
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
return Object.freeze({
|
|
1056
|
+
schemaVersion: COLLECTOR_TERMINAL_SCHEMA_VERSION,
|
|
1057
|
+
userTurnId: raw.parentStableId,
|
|
1058
|
+
assistantTurnId: raw.stableId,
|
|
1059
|
+
userTurnEvidenceDigest: user.evidenceDigest,
|
|
1060
|
+
assistantTurnEvidenceDigest: normalized.evidenceDigest,
|
|
1061
|
+
userOrdinal: user.ordinal,
|
|
1062
|
+
assistantOrdinal: raw.ordinal,
|
|
1063
|
+
branchStableId: raw.branchStableId,
|
|
1064
|
+
text,
|
|
1065
|
+
responseFormat: requestedFormat,
|
|
1066
|
+
...(options.responseContent === "include" && options.rawAssistantTurnId === raw.stableId ? { rawText: formatted.text } : {}),
|
|
1067
|
+
artifacts: Object.freeze(artifacts),
|
|
1068
|
+
finishReason: raw.finishReason
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async function makePostSendDelta(baseline, users, evidenceDigest) {
|
|
1072
|
+
const baselineIds = new Set();
|
|
1073
|
+
let baselineCursor = 0;
|
|
1074
|
+
for (const turn of baseline.userTurns) {
|
|
1075
|
+
if (turn.stableId === undefined || baselineIds.has(turn.stableId))
|
|
1076
|
+
throw new BrowserObservationError("unstable_identity");
|
|
1077
|
+
baselineIds.add(turn.stableId);
|
|
1078
|
+
const fresh = users[baselineCursor];
|
|
1079
|
+
if (fresh?.stableId !== turn.stableId || fresh.evidenceDigest !== turn.evidenceDigest)
|
|
1080
|
+
throw new BrowserObservationError("incomplete_dom");
|
|
1081
|
+
baselineCursor += 1;
|
|
1082
|
+
}
|
|
1083
|
+
const added = users.slice(baselineCursor);
|
|
1084
|
+
const addedUserEvidenceDigests = added.map(turn => turn.evidenceDigest);
|
|
1085
|
+
const deltaDigest = await digest(evidenceDigest, "browser-observation-post-send-delta", {
|
|
1086
|
+
baselineSnapshotDigest: baseline.snapshotDigest,
|
|
1087
|
+
addedUserEvidenceDigests
|
|
1088
|
+
});
|
|
1089
|
+
return Object.freeze({
|
|
1090
|
+
baselineSnapshotDigest: baseline.snapshotDigest,
|
|
1091
|
+
addedUserEvidenceDigests: Object.freeze(addedUserEvidenceDigests),
|
|
1092
|
+
deltaDigest
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
async function buildTarget(raw, options) {
|
|
1096
|
+
if (options.target.expectedConversationId !== undefined && options.target.expectedConversationId !== raw.conversationId)
|
|
1097
|
+
throw new BrowserObservationError("navigation_ambiguous");
|
|
1098
|
+
if (options.target.expectedThreadId !== undefined && options.target.expectedThreadId !== raw.threadId)
|
|
1099
|
+
throw new BrowserObservationError("navigation_ambiguous");
|
|
1100
|
+
const hasConversationIdentity = raw.conversationId !== undefined && raw.threadId !== undefined;
|
|
1101
|
+
const canonicalDigest = hasConversationIdentity
|
|
1102
|
+
? await digest(options.evidenceDigest, "browser-observation-url", raw.canonicalUrl)
|
|
1103
|
+
: undefined;
|
|
1104
|
+
const target = {
|
|
1105
|
+
provider: availableIdentity(options.target.providerId),
|
|
1106
|
+
browser: availableIdentity(options.target.browserId),
|
|
1107
|
+
tab: availableIdentity(options.target.tabId),
|
|
1108
|
+
thread: raw.threadId === undefined ? unavailableIdentity("not_observed") : availableIdentity(raw.threadId),
|
|
1109
|
+
conversation: raw.conversationId === undefined ? unavailableIdentity("not_observed") : availableIdentity(raw.conversationId),
|
|
1110
|
+
canonicalThreadUrl: canonicalDigest === undefined
|
|
1111
|
+
? unavailableIdentity("not_observed")
|
|
1112
|
+
: availableUrlIdentity(`${OPAQUE_URL_PREFIX}${canonicalDigest.slice("hmac-sha256:".length)}`),
|
|
1113
|
+
authoritativeTabClaim: options.target.authoritativeTabClaim === undefined
|
|
1114
|
+
? unavailableIdentity("not_exposed")
|
|
1115
|
+
: availableIdentity(options.target.authoritativeTabClaim),
|
|
1116
|
+
coordinationScope: options.target.coordinationScope
|
|
1117
|
+
};
|
|
1118
|
+
return Object.freeze(target);
|
|
1119
|
+
}
|
|
1120
|
+
function targetMaterial(target) {
|
|
1121
|
+
return {
|
|
1122
|
+
provider: target.provider,
|
|
1123
|
+
browser: target.browser,
|
|
1124
|
+
tab: target.tab,
|
|
1125
|
+
thread: target.thread,
|
|
1126
|
+
conversation: target.conversation,
|
|
1127
|
+
canonicalThreadUrl: target.canonicalThreadUrl,
|
|
1128
|
+
authoritativeTabClaim: target.authoritativeTabClaim,
|
|
1129
|
+
coordinationScope: target.coordinationScope
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function turnMaterial(turn) {
|
|
1133
|
+
return {
|
|
1134
|
+
stableId: turn.stableId,
|
|
1135
|
+
evidenceDigest: turn.evidenceDigest,
|
|
1136
|
+
structureDigest: turn.structureDigest,
|
|
1137
|
+
ordinal: turn.ordinal,
|
|
1138
|
+
...(turn.parentStableId === undefined ? {} : { parentStableId: turn.parentStableId }),
|
|
1139
|
+
...(turn.branchStableId === undefined ? {} : { branchStableId: turn.branchStableId }),
|
|
1140
|
+
...(turn.state === undefined ? {} : { state: turn.state }),
|
|
1141
|
+
artifactEvidenceDigests: turn.artifactEvidenceDigests ?? []
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
async function digest(fn, domain, material) {
|
|
1145
|
+
let result;
|
|
1146
|
+
try {
|
|
1147
|
+
result = await fn(domain, material);
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
throw new BrowserObservationError("evidence_digest_failed");
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof result !== "string" || !DIGEST_PATTERN.test(result))
|
|
1153
|
+
throw new BrowserObservationError("evidence_digest_failed");
|
|
1154
|
+
return result;
|
|
1155
|
+
}
|
|
1156
|
+
function availableIdentity(value) {
|
|
1157
|
+
return Object.freeze({ status: "available", value });
|
|
1158
|
+
}
|
|
1159
|
+
function availableUrlIdentity(value) {
|
|
1160
|
+
return Object.freeze({ status: "available", value });
|
|
1161
|
+
}
|
|
1162
|
+
function unavailableIdentity(reason) {
|
|
1163
|
+
return Object.freeze({ status: "unavailable", reason });
|
|
1164
|
+
}
|
|
1165
|
+
function validateRawTurnOrdering(turns) {
|
|
1166
|
+
const counters = { user: 0, assistant: 0 };
|
|
1167
|
+
for (const turn of turns) {
|
|
1168
|
+
if (turn.ordinal !== counters[turn.role])
|
|
1169
|
+
throw new BrowserObservationError("incomplete_dom");
|
|
1170
|
+
counters[turn.role] += 1;
|
|
1171
|
+
}
|
|
1172
|
+
const users = new Set(turns.filter(turn => turn.role === "user").map(turn => turn.stableId));
|
|
1173
|
+
for (const turn of turns) {
|
|
1174
|
+
if (turn.role === "assistant" && (turn.parentStableId === undefined || !users.has(turn.parentStableId)))
|
|
1175
|
+
throw new BrowserObservationError("branch_ambiguous");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function isRecord(value) {
|
|
1179
|
+
return isPlainDataRecord(value);
|
|
1180
|
+
}
|
|
1181
|
+
function assertExactKeys(value, allowed) {
|
|
1182
|
+
const allowedSet = new Set(allowed);
|
|
1183
|
+
for (const key of Object.keys(value)) {
|
|
1184
|
+
if (!allowedSet.has(key))
|
|
1185
|
+
throw new BrowserObservationError("provider_shape_drift");
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function isBoundedId(value) {
|
|
1189
|
+
return typeof value === "string" && ID_PATTERN.test(value);
|
|
1190
|
+
}
|
|
1191
|
+
function isBoundedText(value, max) {
|
|
1192
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000]/u.test(value);
|
|
1193
|
+
}
|
|
1194
|
+
function isBoundedUrl(value) {
|
|
1195
|
+
if (typeof value !== "string" || value.length < 1 || value.length > MAX_ATTRIBUTE_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
|
|
1196
|
+
return false;
|
|
1197
|
+
try {
|
|
1198
|
+
const url = new URL(value);
|
|
1199
|
+
return url.protocol === "https:" && url.username === "" && url.password === "" && url.search === "" && url.hash === "";
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function isContentDigest(value) {
|
|
1206
|
+
return typeof value === "string" && /^(?:hmac-sha256:|sha256:)[0-9a-f]{64}$/.test(value);
|
|
1207
|
+
}
|
|
1208
|
+
function boundedPositive(value, fallback) {
|
|
1209
|
+
return Number.isSafeInteger(value) && value > 0 ? Math.min(value, fallback) : fallback;
|
|
1210
|
+
}
|
|
1211
|
+
function canonicalizeUrl(value) {
|
|
1212
|
+
const url = new URL(value);
|
|
1213
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "")
|
|
1214
|
+
throw new Error("non-canonical navigation");
|
|
1215
|
+
url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/u, "") || "/";
|
|
1216
|
+
return url.toString();
|
|
1217
|
+
}
|
|
1218
|
+
function conversationIdFromUrl(urlValue) {
|
|
1219
|
+
const url = new URL(urlValue);
|
|
1220
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
1221
|
+
const index = parts.findIndex(part => part === "c" || part === "conversation");
|
|
1222
|
+
const candidate = index >= 0 ? parts[index + 1] : undefined;
|
|
1223
|
+
return candidate !== undefined && ID_PATTERN.test(candidate) ? candidate : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
function readTurnState(root, roleNode) {
|
|
1226
|
+
const values = [root, roleNode].flatMap(node => [
|
|
1227
|
+
node.getAttribute("data-generation-state"),
|
|
1228
|
+
node.getAttribute("data-state"),
|
|
1229
|
+
node.getAttribute("data-status"),
|
|
1230
|
+
node.getAttribute("data-turn-state"),
|
|
1231
|
+
node.getAttribute("data-generating") === "true" ? "generating" : node.getAttribute("data-generating") === "false" ? "terminal" : null,
|
|
1232
|
+
node.getAttribute("aria-busy") === "true" ? "generating" : node.getAttribute("aria-busy") === "false" ? "terminal" : null
|
|
1233
|
+
]).filter((value) => value !== null && value !== undefined).map(value => value.trim().toLowerCase());
|
|
1234
|
+
const generating = values.filter(value => ["generating", "streaming", "in_progress", "in-progress", "true"].includes(value));
|
|
1235
|
+
const terminal = values.filter(value => ["terminal", "complete", "completed", "done", "false"].includes(value));
|
|
1236
|
+
if (generating.length > 0 && terminal.length > 0)
|
|
1237
|
+
throw new Error("unstable generation state");
|
|
1238
|
+
if (generating.length > 0)
|
|
1239
|
+
return "generating";
|
|
1240
|
+
if (terminal.length > 0)
|
|
1241
|
+
return "terminal";
|
|
1242
|
+
return undefined;
|
|
1243
|
+
}
|
|
1244
|
+
function readFinishReason(root, roleNode) {
|
|
1245
|
+
for (const node of [root, roleNode]) {
|
|
1246
|
+
for (const name of ["data-finish-reason", "data-stop-reason", "data-completion-reason"]) {
|
|
1247
|
+
const value = node.getAttribute(name);
|
|
1248
|
+
if (value !== null && value.length > 0)
|
|
1249
|
+
return value.trim().slice(0, 128);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return undefined;
|
|
1253
|
+
}
|
|
1254
|
+
function utf8Bytes(value) {
|
|
1255
|
+
return new TextEncoder().encode(value).byteLength;
|
|
1256
|
+
}
|