@agent-native/core 0.225.0 → 0.225.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/harness/lifecycle.js +47 -11
- package/dist/agent/optional-placeholders.js +24 -5
- package/dist/agent/production-agent.js +38 -34
- package/dist/agent/run-manager.js +95 -37
- package/dist/agent/run-store.js +4 -1
- package/dist/client/integrations/workspace-provider-oauth.d.ts +24 -0
- package/dist/client/integrations/workspace-provider-oauth.js +110 -0
- package/dist/framework-package-versions.json +11 -11
- package/dist/server/agent-chat-plugin.js +29 -27
- package/dist/server/auth.js +46 -2
- package/dist/shared/attachments.d.ts +2 -1
- package/dist/shared/attachments.js +2 -13
- package/dist/shared/inline-bytes.js +5 -18
- package/package.json +3 -3
|
@@ -571,6 +571,8 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
571
571
|
let providerDispatchStarted = false;
|
|
572
572
|
const hasFollowUpProviderDispatchStarted = () => providerDispatchStarted;
|
|
573
573
|
let activeRun;
|
|
574
|
+
let finalizedRejected = false;
|
|
575
|
+
const hasFinalizedRejection = () => finalizedRejected;
|
|
574
576
|
try {
|
|
575
577
|
const dispatchIntent = await markAgentHarnessSessionFollowUpDispatched(session.id, claimed.generation, claimId);
|
|
576
578
|
if (!dispatchIntent) {
|
|
@@ -801,7 +803,10 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
801
803
|
rejectFirstDispatch(runFailure ??
|
|
802
804
|
new Error("The AgentHarness follow-up run ended before provider dispatch."));
|
|
803
805
|
}
|
|
804
|
-
},
|
|
806
|
+
}, (error) => {
|
|
807
|
+
finalizedRejected = true;
|
|
808
|
+
rejectFirstDispatch(error);
|
|
809
|
+
});
|
|
805
810
|
if (followUpAbort.signal.aborted)
|
|
806
811
|
abortActiveRun();
|
|
807
812
|
return firstDispatch;
|
|
@@ -819,7 +824,13 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
819
824
|
refreshLiveAgentHarnessSessionGeneration(session.id, acquired.live.session, dispatch.session.generation);
|
|
820
825
|
if (isAbortRequested(followUpAbort.signal))
|
|
821
826
|
abortActiveRun();
|
|
822
|
-
|
|
827
|
+
try {
|
|
828
|
+
await activeRun.finalized;
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
finalizedRejected = true;
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
823
834
|
}
|
|
824
835
|
finally {
|
|
825
836
|
followUpAbort.signal.removeEventListener("abort", abortActiveRun);
|
|
@@ -994,13 +1005,22 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
994
1005
|
if (isAbortRequested(followUpAbort.signal)) {
|
|
995
1006
|
const reason = followUpAbort.signal.reason;
|
|
996
1007
|
if (isAgentHarnessSessionConflictError(reason)) {
|
|
997
|
-
const latest = await getAgentHarnessSessionByRunId(input.runId);
|
|
998
1008
|
const cleanupErrors = [];
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
latest
|
|
1003
|
-
|
|
1009
|
+
let lookupFailed = false;
|
|
1010
|
+
let latest;
|
|
1011
|
+
try {
|
|
1012
|
+
latest = await getAgentHarnessSessionByRunId(input.runId);
|
|
1013
|
+
}
|
|
1014
|
+
catch (lookupError) {
|
|
1015
|
+
lookupFailed = true;
|
|
1016
|
+
cleanupErrors.push(lookupError);
|
|
1017
|
+
}
|
|
1018
|
+
if (latest !== undefined &&
|
|
1019
|
+
(!latest ||
|
|
1020
|
+
(latest.status === "idle" && latest.followUpClaimId == null) ||
|
|
1021
|
+
latest.status === "stopped" ||
|
|
1022
|
+
latest.status === "errored" ||
|
|
1023
|
+
latest.status === "destroyed")) {
|
|
1004
1024
|
try {
|
|
1005
1025
|
await stopAcquiredHarnessSession(session.id, acquired.live.session);
|
|
1006
1026
|
}
|
|
@@ -1016,12 +1036,26 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
1016
1036
|
cleanupErrors.push(closeError);
|
|
1017
1037
|
}
|
|
1018
1038
|
}
|
|
1039
|
+
if (lookupFailed &&
|
|
1040
|
+
!hasFinalizedRejection() &&
|
|
1041
|
+
cleanupErrors.length === 1) {
|
|
1042
|
+
throw cleanupErrors[0];
|
|
1043
|
+
}
|
|
1019
1044
|
if (cleanupErrors.length > 0) {
|
|
1020
|
-
throw new AggregateError([
|
|
1045
|
+
throw new AggregateError([
|
|
1046
|
+
reason,
|
|
1047
|
+
...(hasFinalizedRejection() ? [error] : []),
|
|
1048
|
+
...cleanupErrors,
|
|
1049
|
+
], "Harness follow-up was interrupted and stream cleanup was incomplete.");
|
|
1021
1050
|
}
|
|
1051
|
+
if (hasFinalizedRejection())
|
|
1052
|
+
throw error;
|
|
1022
1053
|
return failure(input.runId, latest ?? session, "unavailable", "The harness follow-up was interrupted because its session claim changed or stopped.");
|
|
1023
1054
|
}
|
|
1024
|
-
const failures = [
|
|
1055
|
+
const failures = [
|
|
1056
|
+
reason ?? error,
|
|
1057
|
+
...(hasFinalizedRejection() ? [error] : []),
|
|
1058
|
+
];
|
|
1025
1059
|
let failedSession = null;
|
|
1026
1060
|
try {
|
|
1027
1061
|
failedSession = await markOwnedFollowUpErrored(session.id, claimId, acquired.live.session, hasFollowUpProviderDispatchStarted() ? "unknown" : "not_dispatched");
|
|
@@ -1046,7 +1080,9 @@ export async function sendAgentHarnessFollowUp(input) {
|
|
|
1046
1080
|
}
|
|
1047
1081
|
}
|
|
1048
1082
|
if (failures.length > 1) {
|
|
1049
|
-
throw new AggregateError(failures,
|
|
1083
|
+
throw new AggregateError(failures, hasFinalizedRejection()
|
|
1084
|
+
? "Harness follow-up claim monitoring failed and finalization or cleanup was incomplete."
|
|
1085
|
+
: "Harness follow-up claim monitoring failed and cleanup was incomplete.");
|
|
1050
1086
|
}
|
|
1051
1087
|
const latest = failedSession ?? (await getAgentHarnessSessionByRunId(input.runId));
|
|
1052
1088
|
return failure(input.runId, latest ?? session, "unavailable", "The harness follow-up was stopped because its session claim could not be verified.");
|
|
@@ -83,14 +83,14 @@ function resolveObjectSchemaBranch(schema, value) {
|
|
|
83
83
|
const index = discriminatedBranchIndex(branches, value);
|
|
84
84
|
return index >= 0 ? branches[index] : undefined;
|
|
85
85
|
}
|
|
86
|
-
function stripOptionalToolPlaceholders(schema, value, emptyValues) {
|
|
86
|
+
function stripOptionalToolPlaceholders(schema, value, emptyValues, depth = 0) {
|
|
87
87
|
if (!schema)
|
|
88
88
|
return { value, changed: false };
|
|
89
89
|
if (Array.isArray(value)) {
|
|
90
90
|
const itemSchema = leafBranches(schema).find((branch) => branch.items)?.items;
|
|
91
91
|
if (!itemSchema)
|
|
92
92
|
return { value, changed: false };
|
|
93
|
-
const results = value.map((item) => stripOptionalToolPlaceholders(itemSchema, item, emptyValues));
|
|
93
|
+
const results = value.map((item) => stripOptionalToolPlaceholders(itemSchema, item, emptyValues, depth + 1));
|
|
94
94
|
return results.some((result) => result.changed)
|
|
95
95
|
? { value: results.map((result) => result.value), changed: true }
|
|
96
96
|
: { value, changed: false };
|
|
@@ -119,6 +119,14 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
|
|
|
119
119
|
}
|
|
120
120
|
continue;
|
|
121
121
|
}
|
|
122
|
+
// Top-level only: a nested null may be a deliberate clear.
|
|
123
|
+
if (emptyValues.stripNull &&
|
|
124
|
+
depth === 0 &&
|
|
125
|
+
entry === null &&
|
|
126
|
+
!required.has(key)) {
|
|
127
|
+
placeholders.push(key);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
122
130
|
if (!required.has(key) && isStructurallyEmptyToolValue(entry)) {
|
|
123
131
|
if (schemaRejectsToolValue(propertySchema, entry)) {
|
|
124
132
|
if (emptyValues.nullOnly && entry !== null)
|
|
@@ -128,9 +136,12 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
|
|
|
128
136
|
continue;
|
|
129
137
|
}
|
|
130
138
|
// A valid `false` sets a value, and `[]` on an action that says an empty
|
|
131
|
-
// list erases nothing is padding.
|
|
139
|
+
// list erases nothing is padding. `null` on an optional field is absent:
|
|
140
|
+
// models fill every unused field with it, so it never marks a call as
|
|
141
|
+
// clearing data. Elsewhere `[]` could wipe a stored
|
|
132
142
|
// list. An empty equal to the schema's own default changes nothing.
|
|
133
143
|
if (entry !== false &&
|
|
144
|
+
entry !== null &&
|
|
134
145
|
!(emptyValues.listsEraseNothing &&
|
|
135
146
|
Array.isArray(entry) &&
|
|
136
147
|
entry.length === 0) &&
|
|
@@ -139,7 +150,7 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
|
|
|
139
150
|
JSON.stringify(propertySchema.default) === JSON.stringify(entry)))
|
|
140
151
|
emptyValues.valid = true;
|
|
141
152
|
}
|
|
142
|
-
const nested = stripOptionalToolPlaceholders(propertySchema, entry, emptyValues);
|
|
153
|
+
const nested = stripOptionalToolPlaceholders(propertySchema, entry, emptyValues, depth + 1);
|
|
143
154
|
if (nested.changed) {
|
|
144
155
|
normalized ??= { ...record };
|
|
145
156
|
normalized[key] = nested.value;
|
|
@@ -175,7 +186,15 @@ export function normalizeOptionalToolPlaceholders(schema, input, entry, { nullOn
|
|
|
175
186
|
nullOnly,
|
|
176
187
|
listsEraseNothing: entry.emptyListsEraseNothing === true,
|
|
177
188
|
};
|
|
178
|
-
|
|
189
|
+
let result = stripOptionalToolPlaceholders(schema, input, emptyValues);
|
|
190
|
+
// A padded call means its nulls are padding too, including on nullable
|
|
191
|
+
// fields where a lone null would be a deliberate clear.
|
|
192
|
+
if (emptyValues.invalid && !emptyValues.valid) {
|
|
193
|
+
result = stripOptionalToolPlaceholders(schema, input, {
|
|
194
|
+
...emptyValues,
|
|
195
|
+
stripNull: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
179
198
|
// Only a declared read is exempt: a dynamic plan-mode effect classifies the
|
|
180
199
|
// raw input, which defaults can turn into a write.
|
|
181
200
|
if (emptyValues.valid &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, parseQueueMessageInput, persistableFilePart, fingerprintAgentSubmissionAttachment, submissionAttachmentsMatch, } from "@agent-native/agentkit/protocol";
|
|
3
|
+
import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, parseQueueMessageInput, persistableFilePart, fingerprintAgentSubmissionAttachment, isPersistableAttachmentUrl, submissionAttachmentsMatch, } from "@agent-native/agentkit/protocol";
|
|
4
4
|
import Ajv from "ajv";
|
|
5
5
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
6
6
|
import { defineEventHandler, getHeader, setResponseHeader, setResponseStatus, getMethod, } from "h3";
|
|
@@ -1412,23 +1412,6 @@ export { DurableAttachmentReferenceRequiredError };
|
|
|
1412
1412
|
function isDataUrlReference(value) {
|
|
1413
1413
|
return typeof value === "string" && /^\s*data:/i.test(value);
|
|
1414
1414
|
}
|
|
1415
|
-
function hasDurableAttachmentUrl(value) {
|
|
1416
|
-
if (typeof value !== "string")
|
|
1417
|
-
return false;
|
|
1418
|
-
const normalized = value.trim();
|
|
1419
|
-
if (!normalized ||
|
|
1420
|
-
isDataUrlReference(normalized) ||
|
|
1421
|
-
isRawBase64Payload(normalized) ||
|
|
1422
|
-
!URL.canParse(normalized)) {
|
|
1423
|
-
return false;
|
|
1424
|
-
}
|
|
1425
|
-
const url = new URL(normalized);
|
|
1426
|
-
return (url.protocol === "https:" &&
|
|
1427
|
-
!url.username &&
|
|
1428
|
-
!url.password &&
|
|
1429
|
-
!url.search &&
|
|
1430
|
-
!url.hash);
|
|
1431
|
-
}
|
|
1432
1415
|
const OMIT_DURABLE_DISPATCH_VALUE = Symbol("omit-durable-dispatch-value");
|
|
1433
1416
|
const DURABLE_ATTACHMENT_PAYLOAD_FIELDS = /^(?:base64|bytes|body|data|dataurl|image|payload)$/i;
|
|
1434
1417
|
const DURABLE_ATTACHMENT_REFERENCE_FIELDS = /^(?:preview|referenceUrl|src|thumbnail|url)$/i;
|
|
@@ -1516,7 +1499,7 @@ function sanitizeDurableAttachment(value, attachmentContext = false, requiredAtt
|
|
|
1516
1499
|
(attachmentReferenceField &&
|
|
1517
1500
|
typeof value === "string" &&
|
|
1518
1501
|
(isInlineBase64Payload(value) ||
|
|
1519
|
-
(value.trim().length > 0 && !
|
|
1502
|
+
(value.trim().length > 0 && !isPersistableAttachmentUrl(value)))))) {
|
|
1520
1503
|
return OMIT_DURABLE_DISPATCH_VALUE;
|
|
1521
1504
|
}
|
|
1522
1505
|
if (Array.isArray(value)) {
|
|
@@ -1537,12 +1520,12 @@ function sanitizeDurableAttachment(value, attachmentContext = false, requiredAtt
|
|
|
1537
1520
|
const isAttachment = attachmentContext || typedAttachment;
|
|
1538
1521
|
const hasInlinePayload = isAttachment && containsDurableAttachmentPayload(item, true);
|
|
1539
1522
|
const hasDurableReference = item.type === "image"
|
|
1540
|
-
?
|
|
1541
|
-
:
|
|
1542
|
-
|
|
1523
|
+
? isPersistableAttachmentUrl(item.url)
|
|
1524
|
+
: isPersistableAttachmentUrl(item.url) ||
|
|
1525
|
+
isPersistableAttachmentUrl(item.referenceUrl);
|
|
1543
1526
|
const hasInlineReference = [item.url, item.referenceUrl].some((reference) => typeof reference === "string" &&
|
|
1544
1527
|
reference.trim().length > 0 &&
|
|
1545
|
-
!
|
|
1528
|
+
!isPersistableAttachmentUrl(reference));
|
|
1546
1529
|
if ((requiredAttachment || typedAttachment) &&
|
|
1547
1530
|
(hasInlinePayload || hasInlineReference) &&
|
|
1548
1531
|
!hasDurableReference) {
|
|
@@ -9040,11 +9023,12 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
|
|
|
9040
9023
|
? { fileId: record.id }
|
|
9041
9024
|
: {}),
|
|
9042
9025
|
});
|
|
9043
|
-
|
|
9044
|
-
|
|
9026
|
+
let uploadAttempted = false;
|
|
9027
|
+
if (inlineData &&
|
|
9045
9028
|
(!part.url || !record.uploadProvider || record.storageUploadFailed)) {
|
|
9046
9029
|
const parsedData = parseBase64DataUrl(inlineData);
|
|
9047
9030
|
if (parsedData) {
|
|
9031
|
+
uploadAttempted = true;
|
|
9048
9032
|
const uploaded = await uploadFile({
|
|
9049
9033
|
data: new Uint8Array(Buffer.from(parsedData.data, "base64")),
|
|
9050
9034
|
filename: attachment.name,
|
|
@@ -9059,15 +9043,16 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
|
|
|
9059
9043
|
name: attachment.name,
|
|
9060
9044
|
...(mediaType ? { mediaType } : {}),
|
|
9061
9045
|
url: uploaded.url,
|
|
9046
|
+
...(part.fileId ? { fileId: part.fileId } : {}),
|
|
9062
9047
|
});
|
|
9063
9048
|
}
|
|
9064
9049
|
}
|
|
9065
9050
|
}
|
|
9066
9051
|
if (requestAttachmentIndexes.has(index)) {
|
|
9067
|
-
if (!part.url || !
|
|
9052
|
+
if (!part.url || !isPersistableAttachmentUrl(part.url))
|
|
9068
9053
|
return null;
|
|
9069
9054
|
const referenceUrl = attachment.referenceUrl;
|
|
9070
|
-
if (referenceUrl && !
|
|
9055
|
+
if (referenceUrl && !isPersistableAttachmentUrl(referenceUrl))
|
|
9071
9056
|
return null;
|
|
9072
9057
|
requestFiles.push({
|
|
9073
9058
|
type: "image",
|
|
@@ -9078,8 +9063,13 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
|
|
|
9078
9063
|
});
|
|
9079
9064
|
continue;
|
|
9080
9065
|
}
|
|
9081
|
-
if (!part.url
|
|
9082
|
-
|
|
9066
|
+
if (!part.url) {
|
|
9067
|
+
// A failed upload is transient (null -> retryable); an id with no
|
|
9068
|
+
// uploadable bytes can never become a reference.
|
|
9069
|
+
if (uploadAttempted)
|
|
9070
|
+
return null;
|
|
9071
|
+
throw new DurableAttachmentReferenceRequiredError();
|
|
9072
|
+
}
|
|
9083
9073
|
files.push(part);
|
|
9084
9074
|
}
|
|
9085
9075
|
return { attachments: files, requestAttachments: requestFiles };
|
|
@@ -9128,12 +9118,12 @@ function validateQueuedPromotionRequestAttachments(value) {
|
|
|
9128
9118
|
return null;
|
|
9129
9119
|
}
|
|
9130
9120
|
}
|
|
9131
|
-
if ((typeof url === "string" && !
|
|
9121
|
+
if ((typeof url === "string" && !isPersistableAttachmentUrl(url)) ||
|
|
9132
9122
|
(typeof attachment.referenceUrl === "string" &&
|
|
9133
|
-
!
|
|
9123
|
+
!isPersistableAttachmentUrl(attachment.referenceUrl)) ||
|
|
9134
9124
|
(typeof data !== "string" &&
|
|
9135
|
-
!
|
|
9136
|
-
!
|
|
9125
|
+
!isPersistableAttachmentUrl(url) &&
|
|
9126
|
+
!isPersistableAttachmentUrl(attachment.referenceUrl))) {
|
|
9137
9127
|
return null;
|
|
9138
9128
|
}
|
|
9139
9129
|
attachments.push(attachment);
|
|
@@ -10339,7 +10329,21 @@ export function createProductionAgentHandler(options) {
|
|
|
10339
10329
|
setResponseStatus(event, 404);
|
|
10340
10330
|
return { error: "Thread not found" };
|
|
10341
10331
|
}
|
|
10342
|
-
|
|
10332
|
+
let attachmentsForQueue;
|
|
10333
|
+
try {
|
|
10334
|
+
attachmentsForQueue = await queueableAttachments(requestAttachments, ownerEmail, submissionRequestAttachmentIndexes);
|
|
10335
|
+
}
|
|
10336
|
+
catch (error) {
|
|
10337
|
+
if (!(error instanceof DurableAttachmentReferenceRequiredError)) {
|
|
10338
|
+
throw error;
|
|
10339
|
+
}
|
|
10340
|
+
setResponseStatus(event, 503);
|
|
10341
|
+
return {
|
|
10342
|
+
error: error.message,
|
|
10343
|
+
code: error.code,
|
|
10344
|
+
retryable: false,
|
|
10345
|
+
};
|
|
10346
|
+
}
|
|
10343
10347
|
if (!attachmentsForQueue) {
|
|
10344
10348
|
if (requestAttachments.some((attachment) => attachment.displayOnly !== true &&
|
|
10345
10349
|
(attachment.type === "image" ||
|
|
@@ -741,6 +741,12 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
741
741
|
let persistenceFailure;
|
|
742
742
|
let persistenceFailureBlocked = false;
|
|
743
743
|
const flushEventWrites = (retryAfterFailure) => {
|
|
744
|
+
if (persistenceFailure instanceof AgentRunEventNotPersistedError &&
|
|
745
|
+
persistenceFailure.terminalRun) {
|
|
746
|
+
const failed = Promise.reject(persistenceFailure);
|
|
747
|
+
persistenceChain = failed;
|
|
748
|
+
return failed;
|
|
749
|
+
}
|
|
744
750
|
if (persistenceDrain) {
|
|
745
751
|
const activeDrain = persistenceDrain;
|
|
746
752
|
const settledDrain = retryAfterFailure
|
|
@@ -779,8 +785,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
779
785
|
catch (error) {
|
|
780
786
|
const terminalRun = error instanceof AgentRunEventNotPersistedError &&
|
|
781
787
|
error.terminalRun;
|
|
782
|
-
persistenceFailure =
|
|
783
|
-
persistenceFailureBlocked =
|
|
788
|
+
persistenceFailure = error;
|
|
789
|
+
persistenceFailureBlocked = true;
|
|
784
790
|
for (const queued of pendingEventWrites) {
|
|
785
791
|
for (const publication of queued.publications) {
|
|
786
792
|
if (publication.settled)
|
|
@@ -1527,6 +1533,14 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1527
1533
|
return publication;
|
|
1528
1534
|
};
|
|
1529
1535
|
const emitRunEvent = (runEvent, options) => {
|
|
1536
|
+
if (persistenceFailure instanceof AgentRunEventNotPersistedError &&
|
|
1537
|
+
persistenceFailure.terminalRun) {
|
|
1538
|
+
const publication = Promise.reject(persistenceFailure);
|
|
1539
|
+
if (!options?.surfacePersistenceError)
|
|
1540
|
+
publication.catch(() => { });
|
|
1541
|
+
void checkSqlAbort();
|
|
1542
|
+
return publication;
|
|
1543
|
+
}
|
|
1530
1544
|
if (privateTurn && runEvent.event.omitFromPersistedHistory !== true) {
|
|
1531
1545
|
runEvent = {
|
|
1532
1546
|
...runEvent,
|
|
@@ -1722,7 +1736,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1722
1736
|
completionInProgress = true;
|
|
1723
1737
|
let completionError = null;
|
|
1724
1738
|
let terminalPersistenceError = null;
|
|
1725
|
-
let
|
|
1739
|
+
let eventPersistenceFailure = null;
|
|
1726
1740
|
let runTerminalErrorCode;
|
|
1727
1741
|
let runTerminalBlockedTool;
|
|
1728
1742
|
let runTerminalBlockedBy;
|
|
@@ -1731,21 +1745,26 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1731
1745
|
let suppressContinuationEvent = false;
|
|
1732
1746
|
try {
|
|
1733
1747
|
await persistenceChain;
|
|
1748
|
+
}
|
|
1749
|
+
catch (error) {
|
|
1750
|
+
eventPersistenceFailure = { error };
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1734
1753
|
await run.abortPersistencePromise;
|
|
1735
1754
|
}
|
|
1736
1755
|
catch (error) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1756
|
+
eventPersistenceFailure ??= { error };
|
|
1757
|
+
}
|
|
1758
|
+
if (eventPersistenceFailure && run.status !== "aborted") {
|
|
1759
|
+
run.status = "errored";
|
|
1760
|
+
pendingTerminalEvent = {
|
|
1761
|
+
seq: allocateRunEventSeq(run),
|
|
1762
|
+
event: {
|
|
1763
|
+
type: "error",
|
|
1764
|
+
error: "Agent run ended unexpectedly",
|
|
1765
|
+
errorCode: "run_event_persistence_failed",
|
|
1766
|
+
},
|
|
1767
|
+
};
|
|
1749
1768
|
}
|
|
1750
1769
|
if (checkpointStatusReadFailed || checkpointUnknownStatus !== null) {
|
|
1751
1770
|
const event = checkpointStatusReadFailed
|
|
@@ -1754,7 +1773,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1754
1773
|
const abortAlreadyWon = run.status === "aborted" && run.abortReason !== undefined;
|
|
1755
1774
|
if (!abortAlreadyWon) {
|
|
1756
1775
|
run.status = "errored";
|
|
1757
|
-
if (!
|
|
1776
|
+
if (!eventPersistenceFailure) {
|
|
1758
1777
|
pendingTerminalEvent = {
|
|
1759
1778
|
seq: peekRunEventSeq(run),
|
|
1760
1779
|
event: { ...event },
|
|
@@ -1768,7 +1787,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1768
1787
|
const resolveTerminalEventForCompletion = () => {
|
|
1769
1788
|
if (run.status === "aborted" && run.abortReason !== undefined)
|
|
1770
1789
|
return null;
|
|
1771
|
-
if (
|
|
1790
|
+
if (eventPersistenceFailure) {
|
|
1772
1791
|
return pendingTerminalEvent
|
|
1773
1792
|
? { ...pendingTerminalEvent, seq: run.events.length }
|
|
1774
1793
|
: null;
|
|
@@ -1886,7 +1905,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1886
1905
|
if (unfinishedTurnContinuationEvent) {
|
|
1887
1906
|
terminalEvent = unfinishedTurnContinuationEvent;
|
|
1888
1907
|
}
|
|
1889
|
-
const terminalReason =
|
|
1908
|
+
const terminalReason = eventPersistenceFailure && finalStatus !== "aborted"
|
|
1890
1909
|
? "error:run_event_persistence_failed"
|
|
1891
1910
|
: privateTurn && terminalEvent?.type === "error"
|
|
1892
1911
|
? "error:private_content_omitted"
|
|
@@ -1989,7 +2008,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1989
2008
|
: error);
|
|
1990
2009
|
};
|
|
1991
2010
|
if (finalStatus === "completed" || finalStatus === "errored") {
|
|
1992
|
-
const terminalEventToEmit =
|
|
2011
|
+
const terminalEventToEmit = eventPersistenceFailure
|
|
1993
2012
|
? {
|
|
1994
2013
|
type: "error",
|
|
1995
2014
|
error: "Agent run ended unexpectedly",
|
|
@@ -2048,7 +2067,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2048
2067
|
? { ...terminalEventToEmit, omitFromPersistedHistory: true }
|
|
2049
2068
|
: terminalEventToEmit,
|
|
2050
2069
|
};
|
|
2051
|
-
if (
|
|
2070
|
+
if (eventPersistenceFailure) {
|
|
2052
2071
|
volatileTerminalEvent = terminal;
|
|
2053
2072
|
}
|
|
2054
2073
|
else {
|
|
@@ -2100,8 +2119,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2100
2119
|
terminalPersistenceEstablished = false;
|
|
2101
2120
|
}
|
|
2102
2121
|
else {
|
|
2122
|
+
let abortStatusAttempted = false;
|
|
2103
2123
|
try {
|
|
2104
2124
|
await insertRunPromise;
|
|
2125
|
+
abortStatusAttempted = true;
|
|
2105
2126
|
const persistAtomicAbort = () => markRunAborted(runId, run.abortReason ?? "execution-lease-claim-failed");
|
|
2106
2127
|
const aborted = executionLeaseClaimFailed
|
|
2107
2128
|
? await persistAtomicAbort()
|
|
@@ -2113,6 +2134,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2113
2134
|
catch (error) {
|
|
2114
2135
|
terminalPersistenceEstablished = false;
|
|
2115
2136
|
reportTerminalStatusPersistenceError(error);
|
|
2137
|
+
if (eventPersistenceFailure &&
|
|
2138
|
+
(abortStatusAttempted || eventPersistenceFailure.error !== error)) {
|
|
2139
|
+
throw new AggregateError([eventPersistenceFailure.error, error], "Run event and abort status persistence both failed");
|
|
2140
|
+
}
|
|
2116
2141
|
throw error;
|
|
2117
2142
|
}
|
|
2118
2143
|
}
|
|
@@ -2136,7 +2161,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2136
2161
|
await insertRunPromise;
|
|
2137
2162
|
}
|
|
2138
2163
|
if (!continuationStatusPersistedBeforeEvent &&
|
|
2139
|
-
(!terminalPersistenceError ||
|
|
2164
|
+
(!terminalPersistenceError || eventPersistenceFailure)) {
|
|
2140
2165
|
let statusUpdated = false;
|
|
2141
2166
|
try {
|
|
2142
2167
|
statusUpdated = await persistOwnTerminalStatus();
|
|
@@ -2208,7 +2233,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2208
2233
|
}
|
|
2209
2234
|
}
|
|
2210
2235
|
if (terminalPersistenceError) {
|
|
2211
|
-
const reconciled =
|
|
2236
|
+
const reconciled = eventPersistenceFailure
|
|
2212
2237
|
? false
|
|
2213
2238
|
: await reconcileTerminalRun();
|
|
2214
2239
|
if (!reconciled)
|
|
@@ -2242,8 +2267,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
2242
2267
|
attemptCount: options?.attemptCount,
|
|
2243
2268
|
});
|
|
2244
2269
|
}
|
|
2245
|
-
if (
|
|
2246
|
-
throw
|
|
2270
|
+
if (eventPersistenceFailure) {
|
|
2271
|
+
throw eventPersistenceFailure.error;
|
|
2247
2272
|
}
|
|
2248
2273
|
})
|
|
2249
2274
|
.finally(async () => {
|
|
@@ -3069,8 +3094,6 @@ export async function abortRunDurably(runId, reason = "user", options) {
|
|
|
3069
3094
|
}
|
|
3070
3095
|
if (run)
|
|
3071
3096
|
run.abortPersistenceManaged = abortPersistedDurably;
|
|
3072
|
-
if (eventFlushFailure)
|
|
3073
|
-
throw eventFlushFailure;
|
|
3074
3097
|
}
|
|
3075
3098
|
catch (error) {
|
|
3076
3099
|
if (run)
|
|
@@ -3090,8 +3113,13 @@ export async function abortRunDurably(runId, reason = "user", options) {
|
|
|
3090
3113
|
},
|
|
3091
3114
|
});
|
|
3092
3115
|
console.error("[run-manager] durable abort persistence failed:", error instanceof Error ? error.message : error);
|
|
3116
|
+
if (eventFlushFailure) {
|
|
3117
|
+
throw new AggregateError([eventFlushFailure, error], "Run abort event flush and marker persistence failed");
|
|
3118
|
+
}
|
|
3093
3119
|
throw error;
|
|
3094
3120
|
}
|
|
3121
|
+
if (eventFlushFailure)
|
|
3122
|
+
throw eventFlushFailure;
|
|
3095
3123
|
return abortedInMemory;
|
|
3096
3124
|
})();
|
|
3097
3125
|
if (run) {
|
|
@@ -3147,7 +3175,7 @@ export async function abortRunAtSafeBoundaryDurably(runId, reason = "user") {
|
|
|
3147
3175
|
throw result.eventFlushFailure;
|
|
3148
3176
|
return result.aborted;
|
|
3149
3177
|
}
|
|
3150
|
-
|
|
3178
|
+
async function abortTurnByRefDurablyInternal(threadId, turnId, reason, reportedFlushFailure) {
|
|
3151
3179
|
const fence = currentExecutionFence();
|
|
3152
3180
|
if (fence && (fence.threadId !== threadId || fence.turnId !== turnId)) {
|
|
3153
3181
|
throw new StaleExecutionFenceError(fence);
|
|
@@ -3161,24 +3189,49 @@ export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
|
|
|
3161
3189
|
runsToFlush.push(run);
|
|
3162
3190
|
}
|
|
3163
3191
|
}
|
|
3164
|
-
const
|
|
3192
|
+
const flushResults = await Promise.allSettled(runsToFlush.map((run) => flushRunEventsBeforeAbort(run)));
|
|
3193
|
+
// Only the same run's repeated flush is already reported by the caller;
|
|
3194
|
+
// a marker write can fail with the very same error object independently.
|
|
3195
|
+
const unreportedFlushFailures = [];
|
|
3196
|
+
let hasRejectedFlush = false;
|
|
3197
|
+
for (const [index, result] of flushResults.entries()) {
|
|
3198
|
+
if (result.status === "rejected") {
|
|
3199
|
+
hasRejectedFlush = true;
|
|
3200
|
+
unreportedFlushFailures.push(result.reason);
|
|
3201
|
+
}
|
|
3202
|
+
else if (result.value &&
|
|
3203
|
+
!(reportedFlushFailure &&
|
|
3204
|
+
runsToFlush[index]?.runId === reportedFlushFailure.runId &&
|
|
3205
|
+
result.value === reportedFlushFailure.error)) {
|
|
3206
|
+
unreportedFlushFailures.push(result.value);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (hasRejectedFlush) {
|
|
3210
|
+
if (unreportedFlushFailures.length === 1)
|
|
3211
|
+
throw unreportedFlushFailures[0];
|
|
3212
|
+
throw new AggregateError(unreportedFlushFailures, "Turn abort event flushes failed");
|
|
3213
|
+
}
|
|
3165
3214
|
let result;
|
|
3166
3215
|
try {
|
|
3167
3216
|
result = await markTurnAborted(threadId, turnId, reason);
|
|
3168
3217
|
}
|
|
3169
3218
|
catch (error) {
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
throw new AggregateError([eventFlushFailure, error], "Turn abort event flush and marker persistence failed");
|
|
3219
|
+
if (unreportedFlushFailures.length) {
|
|
3220
|
+
throw new AggregateError([...unreportedFlushFailures, error], "Turn abort event flush and marker persistence failed");
|
|
3173
3221
|
}
|
|
3174
3222
|
throw error;
|
|
3175
3223
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3224
|
+
if (unreportedFlushFailures.length === 1)
|
|
3225
|
+
throw unreportedFlushFailures[0];
|
|
3226
|
+
if (unreportedFlushFailures.length > 1) {
|
|
3227
|
+
throw new AggregateError(unreportedFlushFailures, "Turn abort event flushes failed");
|
|
3228
|
+
}
|
|
3179
3229
|
return result;
|
|
3180
3230
|
}
|
|
3181
|
-
export async function
|
|
3231
|
+
export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
|
|
3232
|
+
return abortTurnByRefDurablyInternal(threadId, turnId, reason);
|
|
3233
|
+
}
|
|
3234
|
+
async function abortTurnDurablyInternal(runId, reason, reportedFlushFailure) {
|
|
3182
3235
|
const memRun = activeRuns.get(runId);
|
|
3183
3236
|
try {
|
|
3184
3237
|
const ref = memRun
|
|
@@ -3187,7 +3240,7 @@ export async function abortTurnDurably(runId, reason = "user") {
|
|
|
3187
3240
|
if (!ref) {
|
|
3188
3241
|
throw new Error(`Cannot durably abort turn for missing run ${runId}`);
|
|
3189
3242
|
}
|
|
3190
|
-
return await
|
|
3243
|
+
return await abortTurnByRefDurablyInternal(ref.threadId, ref.turnId, reason, reportedFlushFailure);
|
|
3191
3244
|
}
|
|
3192
3245
|
catch (error) {
|
|
3193
3246
|
captureError(error, {
|
|
@@ -3199,6 +3252,9 @@ export async function abortTurnDurably(runId, reason = "user") {
|
|
|
3199
3252
|
throw error;
|
|
3200
3253
|
}
|
|
3201
3254
|
}
|
|
3255
|
+
export async function abortTurnDurably(runId, reason = "user") {
|
|
3256
|
+
return abortTurnDurablyInternal(runId, reason);
|
|
3257
|
+
}
|
|
3202
3258
|
/** Persist a Stop's turn marker even when a terminal event flush must be reported. */
|
|
3203
3259
|
export async function abortRunAndTurnDurably(runId, reason = "user", options) {
|
|
3204
3260
|
let abortedInMemory = false;
|
|
@@ -3222,7 +3278,9 @@ export async function abortRunAndTurnDurably(runId, reason = "user", options) {
|
|
|
3222
3278
|
}
|
|
3223
3279
|
if (!options?.safeBoundary || abortedInMemory) {
|
|
3224
3280
|
try {
|
|
3225
|
-
await
|
|
3281
|
+
await abortTurnDurablyInternal(runId, reason, terminalFlushFailure
|
|
3282
|
+
? { runId, error: terminalFlushFailure }
|
|
3283
|
+
: undefined);
|
|
3226
3284
|
}
|
|
3227
3285
|
catch (error) {
|
|
3228
3286
|
if (terminalFlushFailure) {
|
package/dist/agent/run-store.js
CHANGED
|
@@ -5138,13 +5138,16 @@ async function retryPendingStaleRunThreadProjections(client) {
|
|
|
5138
5138
|
const projection = row;
|
|
5139
5139
|
if (projection.thread_projection_error_code != null) {
|
|
5140
5140
|
failed += 1;
|
|
5141
|
+
console.error(`[run-store] stale projection stuck for run ${String(projection.id)}:`, projection.thread_projection_error_code);
|
|
5141
5142
|
continue;
|
|
5142
5143
|
}
|
|
5143
5144
|
const runId = projection.id;
|
|
5144
5145
|
if (typeof runId !== "string")
|
|
5145
5146
|
continue;
|
|
5146
|
-
if (!(await finalizeStaleRun(runId, "stale-projection-retry")))
|
|
5147
|
+
if (!(await finalizeStaleRun(runId, "stale-projection-retry"))) {
|
|
5147
5148
|
failed += 1;
|
|
5149
|
+
console.error(`[run-store] stale projection retry failed for run ${runId}`);
|
|
5150
|
+
}
|
|
5148
5151
|
}
|
|
5149
5152
|
return { failed, truncated };
|
|
5150
5153
|
}
|
|
@@ -15,4 +15,28 @@ export interface WorkspaceProviderOAuthOptions {
|
|
|
15
15
|
targetAccountEmail?: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function workspaceProviderOAuthUrl(provider: string, options: WorkspaceProviderOAuthOptions): string;
|
|
18
|
+
/** True when this app runs inside another page's iframe (for example Dispatch). */
|
|
19
|
+
export declare function isFramedContext(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The completion route the OAuth popup returns to. Written in the
|
|
22
|
+
* mount-relative, internal `/_agent-native` form: the app's server joins it to
|
|
23
|
+
* its own mount (see `getAppUrl`), so a path-mounted deployment does not double
|
|
24
|
+
* the prefix. The completion page posts the workspace-connection message back
|
|
25
|
+
* to the opener.
|
|
26
|
+
*/
|
|
27
|
+
export declare const WORKSPACE_CONNECTION_COMPLETION_RETURN = "/_agent-native/oauth/popup?complete=workspace-connection";
|
|
28
|
+
/**
|
|
29
|
+
* The start URL for a popup that returns to the framework completion page, so
|
|
30
|
+
* an embedded app can learn the flow finished instead of navigating its frame
|
|
31
|
+
* to the provider (which providers refuse to be framed). The caller's own
|
|
32
|
+
* `returnPath` is preserved in the options and applied on completion.
|
|
33
|
+
*/
|
|
34
|
+
export declare function workspaceProviderOAuthPopupStartUrl(provider: string, options: WorkspaceProviderOAuthOptions): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve when the popup posts the framework completion message, or when it is
|
|
37
|
+
* closed without one. Accepts the completion page's origin, which can be a
|
|
38
|
+
* configured API origin distinct from the embedded UI origin, while still
|
|
39
|
+
* binding to the exact popup window. Returns a stop function.
|
|
40
|
+
*/
|
|
41
|
+
export declare function watchWorkspaceConnectionCompletion(popup: Window, onComplete: () => void, expectedOrigin?: string): () => void;
|
|
18
42
|
export declare function startWorkspaceProviderOAuth(provider: string, options: WorkspaceProviderOAuthOptions): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../../shared/framework-route-prefix.js";
|
|
1
2
|
import { agentNativePath } from "../api-path.js";
|
|
3
|
+
import { openOAuthPopup } from "../oauth-popup.js";
|
|
2
4
|
import { serverMountReturnPath } from "../sign-in-return.js";
|
|
3
5
|
export function workspaceProviderOAuthUrl(provider, options) {
|
|
4
6
|
const params = new URLSearchParams({
|
|
@@ -22,6 +24,114 @@ export function workspaceProviderOAuthUrl(provider, options) {
|
|
|
22
24
|
}
|
|
23
25
|
return agentNativePath(`/_agent-native/connections/oauth/${provider}/start?${params.toString()}`);
|
|
24
26
|
}
|
|
27
|
+
/** True when this app runs inside another page's iframe (for example Dispatch). */
|
|
28
|
+
export function isFramedContext() {
|
|
29
|
+
if (typeof window === "undefined")
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return window.top !== window.self;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A cross-origin ancestor makes `window.top` throw; that means framed.
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The completion route the OAuth popup returns to. Written in the
|
|
41
|
+
* mount-relative, internal `/_agent-native` form: the app's server joins it to
|
|
42
|
+
* its own mount (see `getAppUrl`), so a path-mounted deployment does not double
|
|
43
|
+
* the prefix. The completion page posts the workspace-connection message back
|
|
44
|
+
* to the opener.
|
|
45
|
+
*/
|
|
46
|
+
export const WORKSPACE_CONNECTION_COMPLETION_RETURN = `${FRAMEWORK_INTERNAL_ROUTE_PREFIX}/oauth/popup?complete=workspace-connection`;
|
|
47
|
+
/** The origin that will serve the completion page and post the message. */
|
|
48
|
+
function completionOrigin(startUrl) {
|
|
49
|
+
if (!URL.canParse(startUrl, window.location.href))
|
|
50
|
+
return undefined;
|
|
51
|
+
return new URL(startUrl, window.location.href).origin;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The start URL for a popup that returns to the framework completion page, so
|
|
55
|
+
* an embedded app can learn the flow finished instead of navigating its frame
|
|
56
|
+
* to the provider (which providers refuse to be framed). The caller's own
|
|
57
|
+
* `returnPath` is preserved in the options and applied on completion.
|
|
58
|
+
*/
|
|
59
|
+
export function workspaceProviderOAuthPopupStartUrl(provider, options) {
|
|
60
|
+
return workspaceProviderOAuthUrl(provider, {
|
|
61
|
+
...options,
|
|
62
|
+
serverReturnPath: WORKSPACE_CONNECTION_COMPLETION_RETURN,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve when the popup posts the framework completion message, or when it is
|
|
67
|
+
* closed without one. Accepts the completion page's origin, which can be a
|
|
68
|
+
* configured API origin distinct from the embedded UI origin, while still
|
|
69
|
+
* binding to the exact popup window. Returns a stop function.
|
|
70
|
+
*/
|
|
71
|
+
export function watchWorkspaceConnectionCompletion(popup, onComplete, expectedOrigin) {
|
|
72
|
+
let closedTicks = 0;
|
|
73
|
+
const stop = () => {
|
|
74
|
+
window.removeEventListener("message", onMessage);
|
|
75
|
+
window.clearInterval(closedTimer);
|
|
76
|
+
};
|
|
77
|
+
const onMessage = (event) => {
|
|
78
|
+
const originAccepted = event.origin === window.location.origin ||
|
|
79
|
+
(expectedOrigin !== undefined && event.origin === expectedOrigin);
|
|
80
|
+
if (!originAccepted || event.source !== popup)
|
|
81
|
+
return;
|
|
82
|
+
const data = event.data;
|
|
83
|
+
if (typeof data !== "object" ||
|
|
84
|
+
data === null ||
|
|
85
|
+
data.type !==
|
|
86
|
+
"agent-native:workspace-connection-complete")
|
|
87
|
+
return;
|
|
88
|
+
stop();
|
|
89
|
+
onComplete();
|
|
90
|
+
};
|
|
91
|
+
// The completion message is posted just before the popup closes; wait one
|
|
92
|
+
// extra tick so a close never wins the race against it.
|
|
93
|
+
const closedTimer = window.setInterval(() => {
|
|
94
|
+
if (!popup.closed) {
|
|
95
|
+
closedTicks = 0;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
closedTicks += 1;
|
|
99
|
+
if (closedTicks > 1)
|
|
100
|
+
stop();
|
|
101
|
+
}, 500);
|
|
102
|
+
window.addEventListener("message", onMessage);
|
|
103
|
+
return stop;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Apply the caller's return target on completion, so resume state carried in
|
|
107
|
+
* `returnPath` survives an embedded flow. Falls back to a reload.
|
|
108
|
+
*/
|
|
109
|
+
function applyReturnPath(options) {
|
|
110
|
+
if (options.returnPath) {
|
|
111
|
+
window.location.assign(options.returnPath);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
window.location.reload();
|
|
115
|
+
}
|
|
25
116
|
export function startWorkspaceProviderOAuth(provider, options) {
|
|
117
|
+
if (isFramedContext()) {
|
|
118
|
+
// Providers refuse to be framed (X-Frame-Options / frame-ancestors), so a
|
|
119
|
+
// same-window navigation inside Dispatch's iframe is refused. Leave the
|
|
120
|
+
// frame: run the flow in a popup whose completion page posts back.
|
|
121
|
+
const startUrl = workspaceProviderOAuthPopupStartUrl(provider, options);
|
|
122
|
+
const popup = openOAuthPopup({
|
|
123
|
+
initialUrl: startUrl,
|
|
124
|
+
features: "width=640,height=760",
|
|
125
|
+
});
|
|
126
|
+
if (popup) {
|
|
127
|
+
watchWorkspaceConnectionCompletion(popup, () => applyReturnPath(options), completionOrigin(startUrl));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Popup blocked while embedded. Never navigate this frame into the
|
|
131
|
+
// provider — the frame refuses it and the user cannot connect at all.
|
|
132
|
+
// Surface the recovery; the caller retries once popups are allowed.
|
|
133
|
+
console.warn("[agent-native] OAuth popup was blocked while embedded in another page; allow popups for this site to connect the provider.");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
26
136
|
window.location.assign(workspaceProviderOAuthUrl(provider, options));
|
|
27
137
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@agent-native/agentkit": "0.225.
|
|
2
|
+
"@agent-native/agentkit": "0.225.1",
|
|
3
3
|
"@agent-native/code-agents-ui": "0.1.75",
|
|
4
|
-
"@agent-native/core": "0.225.
|
|
5
|
-
"@agent-native/core-corpus": "0.225.
|
|
6
|
-
"@agent-native/creative-context": "0.8.
|
|
7
|
-
"@agent-native/dispatch": "0.42.
|
|
4
|
+
"@agent-native/core": "0.225.1",
|
|
5
|
+
"@agent-native/core-corpus": "0.225.1",
|
|
6
|
+
"@agent-native/creative-context": "0.8.67",
|
|
7
|
+
"@agent-native/dispatch": "0.42.2",
|
|
8
8
|
"@agent-native/embedding": "0.2.0",
|
|
9
9
|
"@agent-native/migrate": "0.0.7",
|
|
10
|
-
"@agent-native/otel": "0.1.
|
|
11
|
-
"@agent-native/pinpoint": "0.1.
|
|
12
|
-
"@agent-native/recap-cli": "0.5.
|
|
13
|
-
"@agent-native/scheduling": "0.2.
|
|
10
|
+
"@agent-native/otel": "0.1.34",
|
|
11
|
+
"@agent-native/pinpoint": "0.1.110",
|
|
12
|
+
"@agent-native/recap-cli": "0.5.98",
|
|
13
|
+
"@agent-native/scheduling": "0.2.54",
|
|
14
14
|
"@agent-native/shared-app-config": "0.1.2",
|
|
15
|
-
"@agent-native/skills": "0.4.
|
|
16
|
-
"@agent-native/toolkit": "0.225.
|
|
15
|
+
"@agent-native/skills": "0.4.2",
|
|
16
|
+
"@agent-native/toolkit": "0.225.1"
|
|
17
17
|
}
|
|
@@ -8167,28 +8167,38 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
8167
8167
|
jobsSkippedReason: "unclaimed-background-sweep-failed",
|
|
8168
8168
|
};
|
|
8169
8169
|
}
|
|
8170
|
+
const incompleteSteps = [
|
|
8171
|
+
staleRunReapIncomplete && "staleRunsReaped",
|
|
8172
|
+
agentHarnessFollowUpRecoveryIncomplete &&
|
|
8173
|
+
"expiredAgentHarnessFollowUps",
|
|
8174
|
+
agentHarnessApprovalRecoveryIncomplete &&
|
|
8175
|
+
"expiredAgentHarnessApprovals",
|
|
8176
|
+
appSweepHandlers.failed.length > 0 && "appSweepHandlers",
|
|
8177
|
+
automationFailureAlerts === null && "automationFailureAlerts",
|
|
8178
|
+
(queuedSubmissionDrain === null ||
|
|
8179
|
+
queuedSubmissionDrain.failed > 0) &&
|
|
8180
|
+
"queuedSubmissionDrain",
|
|
8181
|
+
(staleAgentTeamRuns === null || staleAgentTeamRuns.failed > 0) &&
|
|
8182
|
+
"staleAgentTeamRuns",
|
|
8183
|
+
].filter((step) => typeof step === "string");
|
|
8184
|
+
if (incompleteSteps.length > 0) {
|
|
8185
|
+
console.error("[recurring-jobs] Sweep incomplete:", {
|
|
8186
|
+
incompleteSteps,
|
|
8187
|
+
staleRunsReaped,
|
|
8188
|
+
expiredAgentHarnessFollowUps,
|
|
8189
|
+
expiredAgentHarnessApprovals,
|
|
8190
|
+
staleAgentTeamRuns,
|
|
8191
|
+
queuedSubmissionDrain,
|
|
8192
|
+
automationFailureAlerts,
|
|
8193
|
+
appSweepHandlers,
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8170
8196
|
if (!triggerAvailability.available) {
|
|
8171
|
-
if (
|
|
8172
|
-
agentHarnessFollowUpRecoveryIncomplete ||
|
|
8173
|
-
agentHarnessApprovalRecoveryIncomplete ||
|
|
8174
|
-
appSweepHandlers.failed.length > 0 ||
|
|
8175
|
-
automationFailureAlerts === null ||
|
|
8176
|
-
queuedSubmissionDrain === null ||
|
|
8177
|
-
queuedSubmissionDrain.failed > 0 ||
|
|
8178
|
-
staleAgentTeamRuns === null ||
|
|
8179
|
-
staleAgentTeamRuns.failed > 0) {
|
|
8197
|
+
if (incompleteSteps.length > 0) {
|
|
8180
8198
|
setResponseStatus(event, 500);
|
|
8181
8199
|
}
|
|
8182
8200
|
return {
|
|
8183
|
-
ok:
|
|
8184
|
-
!agentHarnessFollowUpRecoveryIncomplete &&
|
|
8185
|
-
!agentHarnessApprovalRecoveryIncomplete &&
|
|
8186
|
-
appSweepHandlers.failed.length === 0 &&
|
|
8187
|
-
automationFailureAlerts !== null &&
|
|
8188
|
-
queuedSubmissionDrain !== null &&
|
|
8189
|
-
queuedSubmissionDrain.failed === 0 &&
|
|
8190
|
-
staleAgentTeamRuns !== null &&
|
|
8191
|
-
staleAgentTeamRuns.failed === 0,
|
|
8201
|
+
ok: incompleteSteps.length === 0,
|
|
8192
8202
|
staleRunsReaped,
|
|
8193
8203
|
expiredAgentHarnessFollowUps,
|
|
8194
8204
|
expiredAgentHarnessApprovals,
|
|
@@ -8204,15 +8214,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
8204
8214
|
}
|
|
8205
8215
|
try {
|
|
8206
8216
|
await processRecurringJobs(schedulerDeps);
|
|
8207
|
-
if (
|
|
8208
|
-
agentHarnessFollowUpRecoveryIncomplete ||
|
|
8209
|
-
agentHarnessApprovalRecoveryIncomplete ||
|
|
8210
|
-
appSweepHandlers.failed.length > 0 ||
|
|
8211
|
-
automationFailureAlerts === null ||
|
|
8212
|
-
queuedSubmissionDrain === null ||
|
|
8213
|
-
queuedSubmissionDrain.failed > 0 ||
|
|
8214
|
-
staleAgentTeamRuns === null ||
|
|
8215
|
-
staleAgentTeamRuns.failed > 0) {
|
|
8217
|
+
if (incompleteSteps.length > 0) {
|
|
8216
8218
|
setResponseStatus(event, 500);
|
|
8217
8219
|
return {
|
|
8218
8220
|
ok: false,
|
package/dist/server/auth.js
CHANGED
|
@@ -3211,11 +3211,38 @@ function createAuthGuardFn(app) {
|
|
|
3211
3211
|
if (session) {
|
|
3212
3212
|
const method = getMethod(event);
|
|
3213
3213
|
if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
|
|
3214
|
+
// A fresh loopback dev workspace has no session org yet, and the
|
|
3215
|
+
// read-only session resolver never creates one, so the app-access check
|
|
3216
|
+
// below can deny before the ordinary org initializer runs. Resolve (or
|
|
3217
|
+
// create) the existing default org through the normal policy first and
|
|
3218
|
+
// hand its id to the unchanged check. The check still owns the verdict;
|
|
3219
|
+
// a missing or failed org initialization also denies protected requests,
|
|
3220
|
+
// while null keeps the wrapper's org-recovery exemption.
|
|
3221
|
+
let workspaceOrgId = session.orgId;
|
|
3222
|
+
let workspaceOrgInitializationFailed = false;
|
|
3223
|
+
if (!workspaceOrgId &&
|
|
3224
|
+
!hasExplicitPersonalOrgScope(event) &&
|
|
3225
|
+
isSyntheticAutoDevIdentity(session.email) &&
|
|
3226
|
+
isLoopbackDevRequest(event) &&
|
|
3227
|
+
getAppConfig().access.orgCreation === "open" &&
|
|
3228
|
+
getAppConfig().access.autoCreateDefaultOrg) {
|
|
3229
|
+
const { getOrgContext } = await import("../org/context.js");
|
|
3230
|
+
try {
|
|
3231
|
+
const org = await getOrgContext(event);
|
|
3232
|
+
if (org.orgId)
|
|
3233
|
+
workspaceOrgId = org.orgId;
|
|
3234
|
+
else
|
|
3235
|
+
workspaceOrgInitializationFailed = true;
|
|
3236
|
+
}
|
|
3237
|
+
catch {
|
|
3238
|
+
workspaceOrgInitializationFailed = true;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3214
3241
|
let workspaceAppAccess = await checkWorkspaceAppAccessForRequest({
|
|
3215
3242
|
path: p,
|
|
3216
3243
|
method,
|
|
3217
3244
|
email: session.email,
|
|
3218
|
-
...(
|
|
3245
|
+
...(workspaceOrgId === undefined ? {} : { orgId: workspaceOrgId }),
|
|
3219
3246
|
});
|
|
3220
3247
|
if (workspaceAppAccess === false &&
|
|
3221
3248
|
!session.orgId &&
|
|
@@ -3246,7 +3273,8 @@ function createAuthGuardFn(app) {
|
|
|
3246
3273
|
setResponseStatus(event, 503);
|
|
3247
3274
|
return workspaceAppAccessUnavailableBody(workspaceAppAccess);
|
|
3248
3275
|
}
|
|
3249
|
-
if (workspaceAppAccess
|
|
3276
|
+
if ((workspaceOrgInitializationFailed && workspaceAppAccess !== null) ||
|
|
3277
|
+
workspaceAppAccess === false) {
|
|
3250
3278
|
setResponseStatus(event, 403);
|
|
3251
3279
|
return { error: "You do not have access to this workspace app." };
|
|
3252
3280
|
}
|
|
@@ -3408,6 +3436,22 @@ export function isLocalDevAuthAllowed(event) {
|
|
|
3408
3436
|
((!isHostedPreviewRequest(event) && isLoopbackRequest(event)) ||
|
|
3409
3437
|
isBuilderPreviewLocalDevRequest(event)));
|
|
3410
3438
|
}
|
|
3439
|
+
/**
|
|
3440
|
+
* A real loopback dev/test request. `isLocalDevAuthAllowed` also admits the
|
|
3441
|
+
* Builder-preview branch, which does not require loopback, so org
|
|
3442
|
+
* auto-initialization additionally requires the actual socket peer and
|
|
3443
|
+
* excludes hosted-preview hosts. Builder preview keeps the ordinary
|
|
3444
|
+
* app-access path.
|
|
3445
|
+
*/
|
|
3446
|
+
function isLoopbackDevRequest(event) {
|
|
3447
|
+
return (isLocalDevAuthAllowed(event) &&
|
|
3448
|
+
isLoopbackRequest(event) &&
|
|
3449
|
+
!isHostedPreviewRequest(event));
|
|
3450
|
+
}
|
|
3451
|
+
/** The framework's own throwaway local account, never a real signup. */
|
|
3452
|
+
function isSyntheticAutoDevIdentity(email) {
|
|
3453
|
+
return (email === AUTO_DEV_ACCOUNT_EMAIL || email === LEGACY_AUTO_DEV_ACCOUNT_EMAIL);
|
|
3454
|
+
}
|
|
3411
3455
|
async function createOrReuseAutoDevSession(auth, config) {
|
|
3412
3456
|
let session = await createBetterAuthSessionForEmail(AUTO_DEV_ACCOUNT_EMAIL, config);
|
|
3413
3457
|
if (!session) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPersistableAttachmentUrl } from "@agent-native/agentkit/protocol";
|
|
2
|
+
export { isPersistableAttachmentUrl };
|
|
1
3
|
export declare function isInlineDataUrl(value: unknown): value is string;
|
|
2
|
-
export declare function isPersistableAttachmentUrl(value: unknown): value is string;
|
|
3
4
|
export declare function stripInlineAttachmentPayloads(value: unknown, attachmentContext?: boolean): unknown;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPersistableAttachmentUrl } from "@agent-native/agentkit/protocol";
|
|
2
|
+
export { isPersistableAttachmentUrl };
|
|
1
3
|
const ATTACHMENT_BODY_FIELDS = new Set([
|
|
2
4
|
"base64",
|
|
3
5
|
"bytes",
|
|
@@ -30,19 +32,6 @@ const ATTACHMENT_CONTEXT_FIELDS = new Set([
|
|
|
30
32
|
export function isInlineDataUrl(value) {
|
|
31
33
|
return typeof value === "string" && /^\s*data:/i.test(value);
|
|
32
34
|
}
|
|
33
|
-
export function isPersistableAttachmentUrl(value) {
|
|
34
|
-
if (typeof value !== "string" || !value.trim())
|
|
35
|
-
return false;
|
|
36
|
-
if (!URL.canParse(value))
|
|
37
|
-
return false;
|
|
38
|
-
const url = new URL(value);
|
|
39
|
-
return (url.protocol === "https:" &&
|
|
40
|
-
Boolean(url.hostname) &&
|
|
41
|
-
!url.username &&
|
|
42
|
-
!url.password &&
|
|
43
|
-
!url.search &&
|
|
44
|
-
!url.hash);
|
|
45
|
-
}
|
|
46
35
|
function isBase64Payload(value) {
|
|
47
36
|
return (typeof value === "string" &&
|
|
48
37
|
value.trim().length >= 64 &&
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
+
import { isPersistableAttachmentReferenceUrl } from "@agent-native/agentkit/protocol";
|
|
1
2
|
import { isInlineDataUrl } from "./attachments.js";
|
|
2
3
|
import { parseDataUrl } from "./data-url.js";
|
|
3
|
-
function isPersistableAttachmentReferenceUrl(value) {
|
|
4
|
-
if (typeof value !== "string" || !value.trim() || value.length > 8192) {
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
7
|
-
if (!URL.canParse(value))
|
|
8
|
-
return false;
|
|
9
|
-
const url = new URL(value);
|
|
10
|
-
return ((url.protocol === "http:" || url.protocol === "https:") &&
|
|
11
|
-
Boolean(url.hostname) &&
|
|
12
|
-
!url.username &&
|
|
13
|
-
!url.password &&
|
|
14
|
-
!url.search &&
|
|
15
|
-
!url.hash);
|
|
16
|
-
}
|
|
17
4
|
/**
|
|
18
5
|
* Inline file bytes reached a SQL write with no durable URL to stand in for
|
|
19
6
|
* them. Raised only by the `reject` policy; a durable dispatch payload must
|
|
@@ -177,10 +164,10 @@ function sanitize(value, policy, inAttachmentList, inheritedUrl, inAttachmentMet
|
|
|
177
164
|
const ownUrl = hasAttachmentContext ? durableUrl(item) : undefined;
|
|
178
165
|
const fallbackUrl = ownUrl ?? (hasAttachmentContext ? inheritedUrl : undefined);
|
|
179
166
|
const inline = hasAttachmentContext ? inlineKeys(item) : [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
167
|
+
// A `fileId` names the attachment; nothing resolves it back to bytes, so it
|
|
168
|
+
// never stands in for bytes that need a URL. The dispatch serializer agrees
|
|
169
|
+
// for parts that carry bytes.
|
|
170
|
+
const unreferenced = isAttachment && inline.length > 0 && !fallbackUrl;
|
|
184
171
|
if (unreferenced && policy === "reject") {
|
|
185
172
|
throw new DurableAttachmentReferenceRequiredError();
|
|
186
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.225.
|
|
3
|
+
"version": "0.225.1",
|
|
4
4
|
"description": "The agentic application framework for building autonomous agents with intuitive UIs",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -760,8 +760,8 @@
|
|
|
760
760
|
"yaml": "^2.9.0",
|
|
761
761
|
"yjs": "^13.6.33",
|
|
762
762
|
"zod": "^4.3.6",
|
|
763
|
-
"@agent-native/agentkit": "^0.225.
|
|
764
|
-
"@agent-native/recap-cli": "^0.5.
|
|
763
|
+
"@agent-native/agentkit": "^0.225.1",
|
|
764
|
+
"@agent-native/recap-cli": "^0.5.98"
|
|
765
765
|
},
|
|
766
766
|
"devDependencies": {
|
|
767
767
|
"@ai-sdk/anthropic": "^3.0.71",
|