wtagent 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -7
- package/package.json +5 -4
- package/src/artifacts/artifact-store.js +154 -0
- package/src/audio/music-artifact.js +66 -0
- package/src/audio/native-music-receiver.js +91 -0
- package/src/browser/base-web-adapter.js +354 -50
- package/src/browser/chatgpt-dom.js +179 -0
- package/src/browser/chatgpt-web-adapter.js +89 -125
- package/src/browser/fake-web-model-adapter.js +16 -1
- package/src/browser/gemini-web-adapter.js +61 -20
- package/src/browser/grok-web-adapter.js +35 -3
- package/src/browser/rendered-text.js +11 -0
- package/src/cli/i18n.js +40 -10
- package/src/cli/main.js +32 -3
- package/src/cli/prompt-input.js +68 -0
- package/src/image/browser-image-driver.js +238 -0
- package/src/image/browser-image-session-pool.js +96 -0
- package/src/image/image-generation-service.js +101 -0
- package/src/image/image-provider-driver.js +22 -0
- package/src/image/native-image-receiver.js +66 -0
- package/src/image/provider-registry.js +23 -0
- package/src/image/providers/chatgpt-image-driver.js +211 -0
- package/src/image/providers/gemini-image-driver.js +100 -0
- package/src/image/providers/grok-image-driver.js +193 -0
- package/src/policy/policy-engine.js +10 -5
- package/src/protocol/envelope.js +74 -0
- package/src/protocol/markers.js +16 -0
- package/src/protocol/prompt-builder.js +12 -9
- package/src/protocol/xml-protocol.js +36 -18
- package/src/runtime/agent-runtime.js +345 -65
- package/src/session/agent-session.js +73 -2
- package/src/tools/image-tools.js +92 -0
- package/src/tools/registry.js +13 -1
- package/docs/technical-design.md +0 -873
|
@@ -5,6 +5,8 @@ import { launchAndConnectCdpChrome } from "./cdp-browser.js";
|
|
|
5
5
|
import { discoverChromeExecutable } from "../platform/chrome-discovery.js";
|
|
6
6
|
import { ensureDirectory } from "../platform/paths.js";
|
|
7
7
|
import { BrowserAdapterError } from "../shared/errors.js";
|
|
8
|
+
import { hasCompleteAgentEnvelope } from "../protocol/envelope.js";
|
|
9
|
+
export { hasCompleteAgentEnvelope } from "../protocol/envelope.js";
|
|
8
10
|
|
|
9
11
|
// Playwright error messages for a dead transport. The Chrome process itself is
|
|
10
12
|
// usually still alive (e.g. the connection died while the Mac slept); these
|
|
@@ -68,17 +70,6 @@ function deltaFrom(previous, current) {
|
|
|
68
70
|
return "";
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
export function hasCompleteAgentEnvelope(text) {
|
|
72
|
-
const trimmed = String(text ?? "").trim();
|
|
73
|
-
const start = trimmed.indexOf("<agent_response");
|
|
74
|
-
const endTag = "</agent_response>";
|
|
75
|
-
const end = trimmed.lastIndexOf(endTag);
|
|
76
|
-
// The envelope is "complete" as soon as both the opening and closing tags
|
|
77
|
-
// are present. The web model may append trailing text or render rich cards
|
|
78
|
-
// after the XML, so we do not require the closing tag to be the last content.
|
|
79
|
-
return start >= 0 && end >= start;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
73
|
function sameConversationUrl(left, right) {
|
|
83
74
|
const normalizedLeft = normalizedConversationUrl(left);
|
|
84
75
|
return normalizedLeft != null
|
|
@@ -220,6 +211,7 @@ export class BaseWebAdapter {
|
|
|
220
211
|
this.lastAssistantMessageId = null;
|
|
221
212
|
this.lastAssistantTurn = null;
|
|
222
213
|
this.lastSendStatus = "not-submitted";
|
|
214
|
+
this.pendingSubmission = null;
|
|
223
215
|
}
|
|
224
216
|
|
|
225
217
|
// ---- provider primitives (override in subclasses) ----------------------
|
|
@@ -342,6 +334,13 @@ export class BaseWebAdapter {
|
|
|
342
334
|
return await message.innerText().catch(() => "");
|
|
343
335
|
}
|
|
344
336
|
|
|
337
|
+
// Virtualized providers can read identities and text in one DOM operation.
|
|
338
|
+
// undefined uses legacy locators; null means a supported sample failed and
|
|
339
|
+
// must be retried, never mixed with a different DOM revision.
|
|
340
|
+
async userMessageSnapshot() {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
345
344
|
// Providers with a shared user/assistant turn order return one atomic snapshot.
|
|
346
345
|
// null keeps legacy normal-send behavior but cannot support ambiguous recovery.
|
|
347
346
|
async orderedConversationSnapshot() {
|
|
@@ -415,17 +414,22 @@ export class BaseWebAdapter {
|
|
|
415
414
|
// Submit the already-filled composer. Providers that need a native pointer
|
|
416
415
|
// sequence may override this while retaining the shared post-send identity
|
|
417
416
|
// checks and one-shot retry.
|
|
418
|
-
async submitComposer(composer) {
|
|
417
|
+
async submitComposer(composer, { assertActive = () => {}, timeoutMs } = {}) {
|
|
418
|
+
assertActive();
|
|
419
419
|
const sendButton = await firstVisible(this.sendButtonLocators());
|
|
420
|
+
assertActive();
|
|
420
421
|
if (sendButton && await sendButton.isEnabled().catch(() => false)) {
|
|
421
422
|
try {
|
|
422
|
-
|
|
423
|
+
assertActive();
|
|
424
|
+
await sendButton.click(timeoutMs == null ? {} : { timeout: timeoutMs });
|
|
423
425
|
return;
|
|
424
426
|
} catch {
|
|
427
|
+
assertActive();
|
|
425
428
|
await this.dismissTransientOverlays();
|
|
426
429
|
}
|
|
427
430
|
}
|
|
428
|
-
|
|
431
|
+
assertActive();
|
|
432
|
+
await composer.press("Enter", timeoutMs == null ? {} : { timeout: timeoutMs });
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
// Scroll a virtualized thread so the latest replies mount. No-op by default.
|
|
@@ -485,10 +489,25 @@ export class BaseWebAdapter {
|
|
|
485
489
|
return new BrowserAdapterError(
|
|
486
490
|
`${this.providerName} did not confirm the submitted message before the causal deadline. `
|
|
487
491
|
+ "It was not sent again automatically.",
|
|
488
|
-
{
|
|
492
|
+
{
|
|
493
|
+
code: "SEND_COMMIT_UNKNOWN",
|
|
494
|
+
recoverable: false,
|
|
495
|
+
details: this.#sendDiagnosticDetails(),
|
|
496
|
+
},
|
|
489
497
|
);
|
|
490
498
|
}
|
|
491
499
|
|
|
500
|
+
#sendDiagnosticDetails() {
|
|
501
|
+
const proof = this.activeSendProof;
|
|
502
|
+
return {
|
|
503
|
+
stage: proof?.stage ?? "unknown",
|
|
504
|
+
sendStatus: this.lastSendStatus,
|
|
505
|
+
elapsedMs: proof?.submittedAt == null
|
|
506
|
+
? null : Math.round(this.monotonicNow() - proof.submittedAt),
|
|
507
|
+
...(proof?.sample ?? {}),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
492
511
|
async #awaitWithinSendDeadline(promise, deadline) {
|
|
493
512
|
const remaining = deadline - this.monotonicNow();
|
|
494
513
|
if (remaining <= 0) {
|
|
@@ -698,7 +717,11 @@ export class BaseWebAdapter {
|
|
|
698
717
|
return true;
|
|
699
718
|
}
|
|
700
719
|
|
|
701
|
-
#captureActiveTurnProof(conversationUrl
|
|
720
|
+
#captureActiveTurnProof(conversationUrl, {
|
|
721
|
+
outboundId = null,
|
|
722
|
+
userMessageId = null,
|
|
723
|
+
userTurn = null,
|
|
724
|
+
} = {}) {
|
|
702
725
|
const normalizedUrl = normalizedConversationUrl(conversationUrl);
|
|
703
726
|
this.activeTurnProof = {
|
|
704
727
|
page: this.page,
|
|
@@ -708,6 +731,9 @@ export class BaseWebAdapter {
|
|
|
708
731
|
targetId: this.cdpChrome?.targetId ?? null,
|
|
709
732
|
scopeEpoch: this.conversationScopeEpoch,
|
|
710
733
|
conversationUrl: normalizedUrl,
|
|
734
|
+
outboundId,
|
|
735
|
+
userMessageId,
|
|
736
|
+
userTurn,
|
|
711
737
|
invalidated: false,
|
|
712
738
|
};
|
|
713
739
|
this.activeTurnConversationUrl = normalizedUrl;
|
|
@@ -903,14 +929,57 @@ export class BaseWebAdapter {
|
|
|
903
929
|
this.expectedRestorationUserId
|
|
904
930
|
&& user.ids.has(this.expectedRestorationUserId)
|
|
905
931
|
);
|
|
906
|
-
|
|
932
|
+
let currentSendUserMatched = Boolean(
|
|
933
|
+
this.freshSendConfirmed
|
|
934
|
+
&& this.lastUserMessageId
|
|
935
|
+
&& user.ids.has(this.lastUserMessageId)
|
|
936
|
+
);
|
|
937
|
+
let currentSendMarker = this.lastUserMessageId;
|
|
938
|
+
if (
|
|
939
|
+
!currentSendUserMatched
|
|
940
|
+
&& this.freshSendConfirmed
|
|
941
|
+
&& isValidOutboundCorrelationId(this.activeTurnProof?.outboundId)
|
|
942
|
+
) {
|
|
943
|
+
const messages = this.userMessages();
|
|
944
|
+
const count = await messages.count().catch(() => 0);
|
|
945
|
+
for (let index = count - 1; index >= 0; index -= 1) {
|
|
946
|
+
const message = messages.nth(index);
|
|
947
|
+
const identity = await this.messageIdentity(message);
|
|
948
|
+
if (
|
|
949
|
+
this.activeTurnProof.userTurn != null
|
|
950
|
+
&& identity.turn != null
|
|
951
|
+
&& identity.turn !== this.activeTurnProof.userTurn
|
|
952
|
+
) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const renderedText = await this.userMessageText(message);
|
|
956
|
+
if (!renderedMessageContainsOutboundMarker(
|
|
957
|
+
renderedText,
|
|
958
|
+
this.activeTurnProof.outboundId,
|
|
959
|
+
)) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
currentSendUserMatched = true;
|
|
963
|
+
currentSendMarker = identity.id
|
|
964
|
+
?? `turn:${identity.turn ?? this.activeTurnProof.userTurn ?? "unknown"}`;
|
|
965
|
+
if (identity.id) {
|
|
966
|
+
this.lastUserMessageId = identity.id;
|
|
967
|
+
this.expectedRestorationUserId = identity.id;
|
|
968
|
+
this.activeTurnProof.userMessageId = identity.id;
|
|
969
|
+
}
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (!assistantMatched && !userMatched && !currentSendUserMatched) {
|
|
907
974
|
this.#resetPendingCanonicalVerification();
|
|
908
975
|
return false;
|
|
909
976
|
}
|
|
910
977
|
|
|
911
|
-
const matchedMarker =
|
|
912
|
-
? `
|
|
913
|
-
:
|
|
978
|
+
const matchedMarker = currentSendUserMatched
|
|
979
|
+
? `user:${currentSendMarker}`
|
|
980
|
+
: assistantMatched
|
|
981
|
+
? `assistant:${this.expectedRestorationAssistantId}`
|
|
982
|
+
: `user:${this.expectedRestorationUserId}`;
|
|
914
983
|
const signature = [
|
|
915
984
|
candidate,
|
|
916
985
|
candidateKind,
|
|
@@ -976,6 +1045,7 @@ export class BaseWebAdapter {
|
|
|
976
1045
|
|
|
977
1046
|
#observeConversationNavigation(value, { navigationEvent = false } = {}) {
|
|
978
1047
|
const normalized = normalizedConversationUrl(value);
|
|
1048
|
+
if (normalized) this.recordConversationNavigation(normalized);
|
|
979
1049
|
if (!normalized) {
|
|
980
1050
|
if (this.activeSendProof) {
|
|
981
1051
|
this.activeSendProof.invalidated = true;
|
|
@@ -997,10 +1067,18 @@ export class BaseWebAdapter {
|
|
|
997
1067
|
// must be observed first for this send epoch.
|
|
998
1068
|
this.activeSendProof.invalidated = true;
|
|
999
1069
|
}
|
|
1000
|
-
if (
|
|
1070
|
+
if (
|
|
1071
|
+
navigationEvent
|
|
1072
|
+
&& this.activeSendProof
|
|
1073
|
+
&& !sameConversationUrl(normalized, this.activeSendProof.currentUrl)
|
|
1074
|
+
) {
|
|
1001
1075
|
// Every top-frame navigation invalidates the current proof until this exact
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1076
|
+
// conversation change is classified as an allowed causal transition below.
|
|
1077
|
+
// Providers can emit several history events for one route (for example
|
|
1078
|
+
// adding a response-id query parameter); those retain the same normalized
|
|
1079
|
+
// conversation identity and must not destroy an otherwise intact proof.
|
|
1080
|
+
// A prior bad transition is permanent; a later plausible route cannot
|
|
1081
|
+
// rehabilitate it.
|
|
1004
1082
|
this.activeSendProof.invalidated = true;
|
|
1005
1083
|
}
|
|
1006
1084
|
this.lastObservedPageUrl = normalized;
|
|
@@ -1706,6 +1784,79 @@ export class BaseWebAdapter {
|
|
|
1706
1784
|
return this.lastAssistantTurn;
|
|
1707
1785
|
}
|
|
1708
1786
|
|
|
1787
|
+
async captureAuxiliaryTurnCompletion({ timeoutMs = 30_000 } = {}) {
|
|
1788
|
+
this.requirePage();
|
|
1789
|
+
const deadline = this.monotonicNow() + Math.max(0, Number(timeoutMs) || 0);
|
|
1790
|
+
let candidate = null;
|
|
1791
|
+
let conversationUrl = null;
|
|
1792
|
+
while (this.monotonicNow() <= deadline) {
|
|
1793
|
+
// A fresh provider chat may allocate its durable conversation URL only
|
|
1794
|
+
// after the generated image has rendered. Keep the exact page/frame/
|
|
1795
|
+
// target binding while the saved user-message marker stabilizes; do not
|
|
1796
|
+
// read or accept an assistant boundary from an uncorrelated candidate URL.
|
|
1797
|
+
if (
|
|
1798
|
+
this.activeTurnConversationUrl
|
|
1799
|
+
&& !this.#activeTurnProofIsIntact({ allowPendingCanonicalUrl: true })
|
|
1800
|
+
) {
|
|
1801
|
+
throw new BrowserAdapterError(
|
|
1802
|
+
`${this.providerName} left the verified conversation during the provider operation.`,
|
|
1803
|
+
{ code: "CONVERSATION_CHANGED_DURING_TURN", recoverable: false },
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const pageState = await this.#submittedPageState();
|
|
1807
|
+
if (pageState.status === "candidate") {
|
|
1808
|
+
await this.waitForPoll(250);
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (pageState.status !== "trusted") {
|
|
1812
|
+
throw new BrowserAdapterError(
|
|
1813
|
+
`${this.providerName} left the verified conversation during the provider operation.`,
|
|
1814
|
+
{ code: "CONVERSATION_CHANGED_DURING_TURN", recoverable: false },
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
conversationUrl = pageState.conversationUrl;
|
|
1818
|
+
this.activeTurnConversationUrl = conversationUrl;
|
|
1819
|
+
if (this.activeTurnProof) {
|
|
1820
|
+
this.activeTurnProof.conversationUrl = conversationUrl;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const messages = this.assistantMessages();
|
|
1824
|
+
const count = await messages.count().catch(() => 0);
|
|
1825
|
+
if (count > 0) {
|
|
1826
|
+
const message = messages.last();
|
|
1827
|
+
const { id, turn } = await this.messageIdentity(message);
|
|
1828
|
+
const text = await this.assistantText(message).catch(() => "");
|
|
1829
|
+
const generating = await this.isAssistantGenerating(message);
|
|
1830
|
+
if (
|
|
1831
|
+
!generating
|
|
1832
|
+
&& this.isNewAssistantIdentity({ id, turn, text, generating: false })
|
|
1833
|
+
) {
|
|
1834
|
+
candidate = { id, turn };
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
await this.waitForPoll(250);
|
|
1839
|
+
}
|
|
1840
|
+
if (!candidate) {
|
|
1841
|
+
throw new BrowserAdapterError(
|
|
1842
|
+
`${this.providerName} did not expose a completed, new assistant boundary for the provider operation.`,
|
|
1843
|
+
{ code: "AUXILIARY_TURN_UNCORRELATED", recoverable: false },
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
await this.#validateRecoveredTurnBoundary({
|
|
1847
|
+
assistantPresent: true,
|
|
1848
|
+
assistantMessageId: candidate.id,
|
|
1849
|
+
assistantTurn: candidate.turn,
|
|
1850
|
+
});
|
|
1851
|
+
this.lastAssistantMessageId = candidate.id ?? null;
|
|
1852
|
+
this.lastAssistantTurn = candidate.turn ?? null;
|
|
1853
|
+
return {
|
|
1854
|
+
assistantMessageId: this.lastAssistantMessageId,
|
|
1855
|
+
assistantTurn: this.lastAssistantTurn,
|
|
1856
|
+
conversationUrl,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1709
1860
|
getLastSendStatus() {
|
|
1710
1861
|
return this.lastSendStatus;
|
|
1711
1862
|
}
|
|
@@ -1715,8 +1866,15 @@ export class BaseWebAdapter {
|
|
|
1715
1866
|
maxBytes = null,
|
|
1716
1867
|
outboundId = null,
|
|
1717
1868
|
allowAssistantContinuation = false,
|
|
1869
|
+
requireAttachments = false,
|
|
1718
1870
|
} = {}) {
|
|
1719
1871
|
this.requirePage();
|
|
1872
|
+
if (this.pendingSubmission) {
|
|
1873
|
+
throw new BrowserAdapterError(
|
|
1874
|
+
"The previous submit action is still in flight. No new message was composed.",
|
|
1875
|
+
{ code: "SEND_COMMIT_UNKNOWN", recoverable: false, details: { stage: "previous-submit" } },
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1720
1878
|
this.lastSendStatus = "not-submitted";
|
|
1721
1879
|
this.expectedAssistantCandidateId = null;
|
|
1722
1880
|
this.expectedAssistantCandidateTurn = null;
|
|
@@ -1792,6 +1950,8 @@ export class BaseWebAdapter {
|
|
|
1792
1950
|
submissionStarted: false,
|
|
1793
1951
|
submittedAt: null,
|
|
1794
1952
|
deadline: null,
|
|
1953
|
+
stage: "prepare",
|
|
1954
|
+
sample: null,
|
|
1795
1955
|
orderedBaseline: null,
|
|
1796
1956
|
freshCandidateUserId: null,
|
|
1797
1957
|
freshCandidateUserTurn: null,
|
|
@@ -1807,6 +1967,16 @@ export class BaseWebAdapter {
|
|
|
1807
1967
|
let attachment = null;
|
|
1808
1968
|
if (files.length > 0) {
|
|
1809
1969
|
attachment = await this.attachFiles(files);
|
|
1970
|
+
if (requireAttachments && (attachment?.failed?.length ?? 0) > 0) {
|
|
1971
|
+
throw new BrowserAdapterError(
|
|
1972
|
+
`${this.providerName} could not attach every required file. Nothing was sent.`,
|
|
1973
|
+
{
|
|
1974
|
+
code: "ATTACHMENT_UPLOAD_FAILED",
|
|
1975
|
+
recoverable: false,
|
|
1976
|
+
details: { attachment },
|
|
1977
|
+
},
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1810
1980
|
}
|
|
1811
1981
|
|
|
1812
1982
|
const assistantMessages = this.assistantMessages();
|
|
@@ -1884,11 +2054,25 @@ export class BaseWebAdapter {
|
|
|
1884
2054
|
sendProof.deadline = sendProof.submittedAt
|
|
1885
2055
|
+ this.sendConfirmationTimeoutMs();
|
|
1886
2056
|
this.lastSendStatus = "commit-unknown";
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
sendProof.
|
|
1890
|
-
|
|
2057
|
+
sendProof.stage = "submit";
|
|
2058
|
+
const assertActive = () => {
|
|
2059
|
+
if (sendProof.invalidated || this.activeSendProof !== sendProof
|
|
2060
|
+
|| this.monotonicNow() >= sendProof.deadline) {
|
|
2061
|
+
throw this.#sendDeadlineError();
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
const submission = Promise.resolve().then(() => this.submitComposer(composer, {
|
|
2065
|
+
assertActive,
|
|
2066
|
+
timeoutMs: Math.max(1, Math.ceil(sendProof.deadline - this.monotonicNow())),
|
|
2067
|
+
}));
|
|
2068
|
+
this.pendingSubmission = submission;
|
|
2069
|
+
const settled = () => {
|
|
2070
|
+
if (this.pendingSubmission === submission) this.pendingSubmission = null;
|
|
2071
|
+
};
|
|
2072
|
+
submission.then(settled, settled);
|
|
2073
|
+
await this.#awaitWithinSendDeadline(submission, sendProof.deadline);
|
|
1891
2074
|
|
|
2075
|
+
sendProof.stage = "user-message-confirmation";
|
|
1892
2076
|
const sentMessage = await this.#waitForSentUserMessage(userBaseline, {
|
|
1893
2077
|
deadline: sendProof.deadline,
|
|
1894
2078
|
expectedText: text,
|
|
@@ -1913,6 +2097,7 @@ export class BaseWebAdapter {
|
|
|
1913
2097
|
this.#observeConversationNavigation(this.page.url());
|
|
1914
2098
|
}
|
|
1915
2099
|
|
|
2100
|
+
sendProof.stage = "conversation-confirmation";
|
|
1916
2101
|
let submittedPage = await this.#awaitWithinSendDeadline(
|
|
1917
2102
|
this.#submittedPageState(),
|
|
1918
2103
|
sendProof.deadline,
|
|
@@ -1938,7 +2123,11 @@ export class BaseWebAdapter {
|
|
|
1938
2123
|
if (this.lastUserMessageId) {
|
|
1939
2124
|
this.expectedRestorationUserId = this.lastUserMessageId;
|
|
1940
2125
|
}
|
|
1941
|
-
this.#captureActiveTurnProof(submittedPage.conversationUrl
|
|
2126
|
+
this.#captureActiveTurnProof(submittedPage.conversationUrl, {
|
|
2127
|
+
outboundId,
|
|
2128
|
+
userMessageId: this.lastUserMessageId,
|
|
2129
|
+
userTurn: sentMessage.turn ?? null,
|
|
2130
|
+
});
|
|
1942
2131
|
this.lastSendStatus = "confirmed";
|
|
1943
2132
|
return {
|
|
1944
2133
|
attachment,
|
|
@@ -1957,6 +2146,20 @@ export class BaseWebAdapter {
|
|
|
1957
2146
|
...assistantBaseline.ids,
|
|
1958
2147
|
],
|
|
1959
2148
|
};
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (error?.code === "SEND_COMMIT_UNKNOWN") {
|
|
2151
|
+
error.completionUnknown = true;
|
|
2152
|
+
error.details = { ...this.#sendDiagnosticDetails(), ...(error.details ?? {}) };
|
|
2153
|
+
// Invalidate before collecting diagnostics: delayed submit callbacks
|
|
2154
|
+
// must not enter a fallback action while this read-only capture runs.
|
|
2155
|
+
if (sendProof) sendProof.invalidated = true;
|
|
2156
|
+
const diagnostics = await this.writeDiagnostics("send-commit-unknown", error.details).catch(() => null);
|
|
2157
|
+
if (diagnostics?.filename) {
|
|
2158
|
+
error.details.diagnosticFile = diagnostics.filename;
|
|
2159
|
+
error.details.dom = diagnostics.report.dom;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
throw error;
|
|
1960
2163
|
} finally {
|
|
1961
2164
|
if (sendProof && this.activeSendProof === sendProof) {
|
|
1962
2165
|
sendProof.invalidated = true;
|
|
@@ -1977,6 +2180,8 @@ export class BaseWebAdapter {
|
|
|
1977
2180
|
truncatedEnvelopeWindowMs = null,
|
|
1978
2181
|
emptyResponseWindowMs = 10_000,
|
|
1979
2182
|
deadRequestGraceMs = 60_000,
|
|
2183
|
+
readNativeImages = null,
|
|
2184
|
+
readNativeMusic = null,
|
|
1980
2185
|
onDelta,
|
|
1981
2186
|
}) {
|
|
1982
2187
|
this.requirePage();
|
|
@@ -1990,6 +2195,8 @@ export class BaseWebAdapter {
|
|
|
1990
2195
|
let sawGenerationSignal = false;
|
|
1991
2196
|
let lastGenerationSignalAt = 0;
|
|
1992
2197
|
let unverifiedPageChecks = 0;
|
|
2198
|
+
let nativeCandidate = null;
|
|
2199
|
+
let nativeStableSince = 0;
|
|
1993
2200
|
const detachEscCancel = this.#attachEscCancel();
|
|
1994
2201
|
const truncatedGraceMs = truncatedEnvelopeWindowMs
|
|
1995
2202
|
?? this.truncatedEnvelopeGraceMs();
|
|
@@ -2131,6 +2338,33 @@ export class BaseWebAdapter {
|
|
|
2131
2338
|
}
|
|
2132
2339
|
}
|
|
2133
2340
|
|
|
2341
|
+
if (readNativeImages || readNativeMusic) {
|
|
2342
|
+
const snapshot = await (readNativeMusic ?? readNativeImages)(lastMessage);
|
|
2343
|
+
const media = readNativeMusic ? snapshot.music : snapshot.images;
|
|
2344
|
+
if (snapshot.pending || media.length > 0) {
|
|
2345
|
+
emptySince = 0;
|
|
2346
|
+
emptyCandidate = null;
|
|
2347
|
+
const signature = JSON.stringify([candidateId, candidateTurn, text, media]);
|
|
2348
|
+
if (generating || snapshot.pending || signature !== nativeCandidate) {
|
|
2349
|
+
nativeCandidate = signature;
|
|
2350
|
+
nativeStableSince = Date.now();
|
|
2351
|
+
} else if (Date.now() - nativeStableSince >= Math.max(2_000, stableWindowMs)) {
|
|
2352
|
+
await this.#validateRecoveredTurnBoundary({
|
|
2353
|
+
assistantPresent: true,
|
|
2354
|
+
assistantMessageId: candidateId,
|
|
2355
|
+
assistantTurn: candidateTurn,
|
|
2356
|
+
});
|
|
2357
|
+
this.lastAssistantMessageId = candidateId;
|
|
2358
|
+
this.lastAssistantTurn = candidateTurn;
|
|
2359
|
+
return readNativeMusic ? { text, nativeMusic: media } : { text, nativeImages: media };
|
|
2360
|
+
}
|
|
2361
|
+
await this.page.waitForTimeout(250);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
nativeCandidate = null;
|
|
2365
|
+
nativeStableSince = 0;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2134
2368
|
if (!text.trim() && !generating) {
|
|
2135
2369
|
// The model can create a real assistant turn and finish it without
|
|
2136
2370
|
// rendering any content. Once that exact empty node remains stopped
|
|
@@ -2615,11 +2849,22 @@ export class BaseWebAdapter {
|
|
|
2615
2849
|
outboundId,
|
|
2616
2850
|
});
|
|
2617
2851
|
} else {
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2852
|
+
const snapshot = await withinDeadline(this.userMessageSnapshot());
|
|
2853
|
+
if (snapshot === null) {
|
|
2854
|
+
candidateBubbleSignature = null;
|
|
2855
|
+
candidateBubbleStableSince = 0;
|
|
2856
|
+
await this.#waitBeforeSendDeadline(deadline);
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
const messages = snapshot === undefined ? this.userMessages() : null;
|
|
2860
|
+
const count = snapshot?.length
|
|
2861
|
+
?? await withinDeadline(messages.count().catch(() => 0));
|
|
2862
|
+
if (this.activeSendProof) {
|
|
2863
|
+
this.activeSendProof.sample = { userMessageCount: count, markerMatched: false };
|
|
2864
|
+
}
|
|
2620
2865
|
for (let index = count - 1; index >= 0; index -= 1) {
|
|
2621
|
-
const message = messages
|
|
2622
|
-
const identity = await withinDeadline(
|
|
2866
|
+
const message = messages?.nth(index);
|
|
2867
|
+
const identity = snapshot?.[index] ?? await withinDeadline(
|
|
2623
2868
|
this.messageIdentity(message),
|
|
2624
2869
|
);
|
|
2625
2870
|
const newByTurn = identity.turn != null
|
|
@@ -2637,12 +2882,23 @@ export class BaseWebAdapter {
|
|
|
2637
2882
|
if (!requireFreshCorrelation && outboundId == null) {
|
|
2638
2883
|
continue;
|
|
2639
2884
|
}
|
|
2640
|
-
const renderedText = await withinDeadline(
|
|
2885
|
+
const renderedText = snapshot?.[index].renderedText ?? await withinDeadline(
|
|
2641
2886
|
this.userMessageText(message),
|
|
2642
2887
|
);
|
|
2888
|
+
const markerMatched = renderedMessageContains(renderedText, expectedText, outboundId);
|
|
2889
|
+
if (this.activeSendProof) {
|
|
2890
|
+
this.activeSendProof.sample = {
|
|
2891
|
+
userMessageCount: count,
|
|
2892
|
+
candidateId: identity.id ?? null,
|
|
2893
|
+
candidateTurn: identity.turn ?? null,
|
|
2894
|
+
candidateIsLast: index === count - 1,
|
|
2895
|
+
renderedTextLength: renderedText.length,
|
|
2896
|
+
markerMatched,
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2643
2899
|
if (
|
|
2644
2900
|
index !== count - 1
|
|
2645
|
-
|| !
|
|
2901
|
+
|| !markerMatched
|
|
2646
2902
|
) {
|
|
2647
2903
|
continue;
|
|
2648
2904
|
}
|
|
@@ -3032,24 +3288,72 @@ export class BaseWebAdapter {
|
|
|
3032
3288
|
throw new BrowserAdapterError("Browser access challenge detected.");
|
|
3033
3289
|
}
|
|
3034
3290
|
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3291
|
+
hasStructuralDiagnostics() {
|
|
3292
|
+
return false;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
recordConversationNavigation(_url) {}
|
|
3296
|
+
|
|
3297
|
+
async structuralDiagnostics() {
|
|
3298
|
+
return null;
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
async collectStructuralDiagnostics() {
|
|
3302
|
+
if (!this.hasStructuralDiagnostics()) return null;
|
|
3303
|
+
let timer;
|
|
3304
|
+
try {
|
|
3305
|
+
return await Promise.race([
|
|
3306
|
+
Promise.resolve().then(() => this.structuralDiagnostics()).catch(() => ({ status: "unavailable" })),
|
|
3307
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve({ status: "timeout" }), 500); }),
|
|
3308
|
+
]);
|
|
3309
|
+
} finally {
|
|
3310
|
+
clearTimeout(timer);
|
|
3038
3311
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
async writeDiagnostics(label, details = null) {
|
|
3315
|
+
if (!this.page || (!this.debug && !this.hasStructuralDiagnostics())) return;
|
|
3316
|
+
const page = this.page;
|
|
3317
|
+
const proof = this.activeSendProof;
|
|
3041
3318
|
const stamp = Date.now();
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3319
|
+
// Capture the stage before awaiting CDP. Never log prompts, correlation IDs,
|
|
3320
|
+
// raw URLs, or arbitrary provider error text in the default JSON report.
|
|
3321
|
+
const report = {
|
|
3322
|
+
schemaVersion: 1,
|
|
3323
|
+
provider: this.providerName,
|
|
3324
|
+
label,
|
|
3325
|
+
capturedAt: new Date(stamp).toISOString(),
|
|
3326
|
+
stage: details?.stage ?? proof?.stage ?? "unknown",
|
|
3327
|
+
sendStatus: this.lastSendStatus,
|
|
3328
|
+
elapsedMs: proof?.submittedAt == null ? null : Math.round(this.monotonicNow() - proof.submittedAt),
|
|
3329
|
+
routes: [...(this.diagnosticRoutes ?? [])],
|
|
3330
|
+
};
|
|
3331
|
+
const directory = path.join(this.profileDir, "..", "diagnostics");
|
|
3332
|
+
const filename = path.join(directory, `${stamp}-${label}.json`);
|
|
3333
|
+
const capture = async () => {
|
|
3334
|
+
const dom = await this.collectStructuralDiagnostics();
|
|
3335
|
+
if (dom) report.dom = dom;
|
|
3336
|
+
if (this.debug && details != null) report.details = details;
|
|
3337
|
+
await ensureDirectory(directory);
|
|
3338
|
+
await fs.writeFile(filename, JSON.stringify(report, null, 2), "utf8");
|
|
3339
|
+
if (this.debug) {
|
|
3340
|
+
await Promise.all([
|
|
3341
|
+
page.screenshot({ path: path.join(directory, `${stamp}-${label}.png`), fullPage: true, timeout: 2_000 }).catch(() => null),
|
|
3342
|
+
page.content().then((html) => fs.writeFile(path.join(directory, `${stamp}-${label}.html`), html, "utf8")).catch(() => null),
|
|
3343
|
+
]);
|
|
3344
|
+
}
|
|
3345
|
+
return { filename, report };
|
|
3346
|
+
};
|
|
3347
|
+
let timer;
|
|
3348
|
+
try {
|
|
3349
|
+
// A dead CDP connection or disk must not mask the original failure.
|
|
3350
|
+
return await Promise.race([
|
|
3351
|
+
capture().catch(() => null),
|
|
3352
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), 2_500); }),
|
|
3353
|
+
]);
|
|
3354
|
+
} finally {
|
|
3355
|
+
clearTimeout(timer);
|
|
3356
|
+
}
|
|
3053
3357
|
}
|
|
3054
3358
|
|
|
3055
3359
|
// Non-private so provider subclasses (e.g. attachFiles) can guard too.
|