@opengeni/worker-bundle 0.27.0-canary.0 → 0.27.2-canary.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/dist/activities/agent-turn/agent-build.d.ts +3 -4
- package/dist/activities/agent-turn/governance-model.d.ts +2 -3
- package/dist/activities/agent-turn/skill-checkout.d.ts +21 -0
- package/dist/activities/agent-turn/skill-install.d.ts +12 -0
- package/dist/activities/agent-turn/skill-read.d.ts +15 -0
- package/dist/activities/agent-turn/skill-save.d.ts +26 -0
- package/dist/activities/agent-turn/skill-search.d.ts +16 -0
- package/dist/activities/agent-turn/skill-selection.d.ts +17 -0
- package/dist/activities/agent-turn/skill-tools.d.ts +20 -0
- package/dist/activities/agent-turn/skill-transfer.d.ts +26 -0
- package/dist/activities/agent-turn/tool-environment.d.ts +7 -0
- package/dist/activities/parent-wake.d.ts +12 -2
- package/dist/activities/run-input.d.ts +11 -5
- package/dist/activities/sandbox-lease.d.ts +5 -4
- package/dist/activities/types.d.ts +4 -2
- package/dist/activities/workflow-wake.d.ts +2 -4
- package/dist/{activities-control-VMA3XMHE.js → activities-control-WZRFEIJH.js} +103 -26
- package/dist/activities-control-WZRFEIJH.js.map +1 -0
- package/dist/{activities-turn-57AOUNYN.js → activities-turn-V7J34ZOF.js} +945 -532
- package/dist/activities-turn-V7J34ZOF.js.map +1 -0
- package/dist/{chunk-D4ZEFOYH.js → chunk-BARTLVBK.js} +1 -1
- package/dist/chunk-BARTLVBK.js.map +1 -0
- package/dist/{chunk-7LS42PHP.js → chunk-C54VDT6I.js} +590 -34
- package/dist/chunk-C54VDT6I.js.map +1 -0
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/model-context-contributions.d.ts +2 -0
- package/dist/observability-metrics.d.ts +1 -1
- package/dist/workflow-bundle.js +1 -1
- package/package.json +17 -17
- package/src/activities/agent-turn/agent-build.ts +8 -34
- package/src/activities/agent-turn/errors.ts +4 -0
- package/src/activities/agent-turn/governance-model.ts +15 -19
- package/src/activities/agent-turn/run.ts +4 -6
- package/src/activities/agent-turn/sandbox-provision.ts +9 -0
- package/src/activities/agent-turn/skill-checkout.ts +124 -0
- package/src/activities/agent-turn/skill-install.ts +57 -0
- package/src/activities/agent-turn/skill-read.ts +117 -0
- package/src/activities/agent-turn/skill-save.ts +124 -0
- package/src/activities/agent-turn/skill-search.ts +110 -0
- package/src/activities/agent-turn/skill-selection.ts +53 -0
- package/src/activities/agent-turn/skill-tools.ts +268 -0
- package/src/activities/agent-turn/skill-transfer.ts +137 -0
- package/src/activities/agent-turn/tool-environment.ts +93 -1
- package/src/activities/automations.ts +3 -1
- package/src/activities/context-compaction.ts +26 -2
- package/src/activities/parent-wake.ts +66 -5
- package/src/activities/run-input.ts +88 -12
- package/src/activities/sandbox-lease.ts +77 -20
- package/src/activities/scheduled-tasks.ts +2 -0
- package/src/activities/types.ts +4 -2
- package/src/activities/workflow-wake.ts +42 -5
- package/src/index.ts +3 -1
- package/src/model-context-contributions.ts +18 -2
- package/src/observability-metrics.ts +2 -0
- package/src/sandbox-routing.ts +42 -1
- package/dist/activities-control-VMA3XMHE.js.map +0 -1
- package/dist/activities-turn-57AOUNYN.js.map +0 -1
- package/dist/chunk-7LS42PHP.js.map +0 -1
- package/dist/chunk-D4ZEFOYH.js.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db";
|
|
2
2
|
import {
|
|
3
3
|
beginConnectorActionExecution,
|
|
4
|
+
getWorkspaceVideoGenerationPolicy,
|
|
5
|
+
listSkillDescriptors,
|
|
4
6
|
completeConnectorActionExecution,
|
|
5
7
|
getScheduledVariableSetExpectedGenerationForAttempt,
|
|
6
8
|
getWorkspaceModelPolicy,
|
|
@@ -86,6 +88,10 @@ import {
|
|
|
86
88
|
} from "./session-title";
|
|
87
89
|
import { resolveTurnSandboxAccess } from "./turn-sandbox-access";
|
|
88
90
|
import { createListModelsAttemptToolDefinition } from "./list-models";
|
|
91
|
+
import { createWorkspaceSkillTools } from "./skill-tools";
|
|
92
|
+
import { loadConfiguredBundledSkills } from "./skill-selection";
|
|
93
|
+
import { guardSkillFilesystem } from "./skill-transfer";
|
|
94
|
+
import type { RuntimeSkillActivation } from "@opengeni/runtime";
|
|
89
95
|
|
|
90
96
|
export type PrepareTurnToolPolicyDeps = {
|
|
91
97
|
input: RunAgentTurnInput;
|
|
@@ -102,6 +108,7 @@ export type PrepareTurnToolPolicyDeps = {
|
|
|
102
108
|
};
|
|
103
109
|
|
|
104
110
|
export type PrepareTurnToolRuntimeDeps = {
|
|
111
|
+
selectedSkillActivations: readonly RuntimeSkillActivation[];
|
|
105
112
|
input: RunAgentTurnInput;
|
|
106
113
|
catalogSourceSettings: Settings;
|
|
107
114
|
db: ActivityServices["db"];
|
|
@@ -555,7 +562,88 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
|
|
|
555
562
|
outcome,
|
|
556
563
|
}),
|
|
557
564
|
};
|
|
565
|
+
const skillConfiguration = await getWorkspaceVideoGenerationPolicy(db, input.workspaceId);
|
|
566
|
+
const bundledSkills = loadConfiguredBundledSkills({
|
|
567
|
+
bundledSkillIds: session.bundledSkillIds,
|
|
568
|
+
firstPartyTools: selectedFirstPartyMcpTools,
|
|
569
|
+
videoGenerationEnabled:
|
|
570
|
+
skillConfiguration.defaultModelId !== null && skillConfiguration.enabledModelIds.length > 0,
|
|
571
|
+
});
|
|
572
|
+
const selectedSkills = [
|
|
573
|
+
...bundledSkills,
|
|
574
|
+
...deps.selectedSkillActivations.map((entry) => ({ id: entry.id, artifact: entry.artifact })),
|
|
575
|
+
...session.skills.map((skill) => ({
|
|
576
|
+
id: `session:${session.id}:${skill.name}`,
|
|
577
|
+
artifact: skill,
|
|
578
|
+
})),
|
|
579
|
+
];
|
|
580
|
+
const sharedSkillDescriptors = await listSkillDescriptors(db, {
|
|
581
|
+
accountId: input.accountId,
|
|
582
|
+
workspaceId: input.workspaceId,
|
|
583
|
+
...(deps.fileAuthoritySubjectId ? { subjectId: deps.fileAuthoritySubjectId } : {}),
|
|
584
|
+
});
|
|
585
|
+
const skillCatalog = [
|
|
586
|
+
...sharedSkillDescriptors
|
|
587
|
+
.filter((entry) => entry.activationMode === "workspace_managed")
|
|
588
|
+
.map((entry) => ({
|
|
589
|
+
id: entry.id,
|
|
590
|
+
name: entry.title,
|
|
591
|
+
description: entry.description,
|
|
592
|
+
})),
|
|
593
|
+
...selectedSkills.map((entry) => ({
|
|
594
|
+
id: entry.id,
|
|
595
|
+
name: entry.artifact.name,
|
|
596
|
+
description: entry.artifact.description || entry.artifact.name,
|
|
597
|
+
})),
|
|
598
|
+
];
|
|
599
|
+
const skillTools = createWorkspaceSkillTools({
|
|
600
|
+
db,
|
|
601
|
+
settings: runSettings,
|
|
602
|
+
accountId: input.accountId,
|
|
603
|
+
workspaceId: input.workspaceId,
|
|
604
|
+
...(deps.fileAuthoritySubjectId ? { subjectId: deps.fileAuthoritySubjectId } : {}),
|
|
605
|
+
actor: {
|
|
606
|
+
kind: "agent",
|
|
607
|
+
sessionId: input.sessionId,
|
|
608
|
+
turnId: turn.id,
|
|
609
|
+
attemptId: input.attemptId,
|
|
610
|
+
executionGeneration: attempt.executionGeneration,
|
|
611
|
+
},
|
|
612
|
+
selected: selectedSkills,
|
|
613
|
+
filesystem: async () => {
|
|
614
|
+
throwIfWorkerShuttingDown();
|
|
615
|
+
throwIfTurnCancelled();
|
|
616
|
+
const access = await resolveTurnSandboxAccess(
|
|
617
|
+
sandboxState,
|
|
618
|
+
media.sdkOwnedSandboxSession,
|
|
619
|
+
"Skill checkout/publish requires a sandbox or Connected Machine.",
|
|
620
|
+
);
|
|
621
|
+
const machineRoot = sandboxState.machinePrimarySession?.workspaceRoot;
|
|
622
|
+
const runAs = sandboxRunAs(runSettings);
|
|
623
|
+
const channel = new SandboxChannelAService({
|
|
624
|
+
session: access.session,
|
|
625
|
+
workspaceRoot: machineRoot ?? "/workspace",
|
|
626
|
+
...(machineRoot ? { providerPathMode: "workspace-relative" as const } : {}),
|
|
627
|
+
leaseEpoch: access.leaseEpoch,
|
|
628
|
+
emit: async (events) => {
|
|
629
|
+
await eventing.publish?.(events, true);
|
|
630
|
+
},
|
|
631
|
+
...(runAs ? { runAs } : {}),
|
|
632
|
+
});
|
|
633
|
+
return guardSkillFilesystem(channel, {
|
|
634
|
+
assertActive: () => {
|
|
635
|
+
throwIfWorkerShuttingDown();
|
|
636
|
+
throwIfTurnCancelled();
|
|
637
|
+
},
|
|
638
|
+
runMutation: async (mutation) =>
|
|
639
|
+
access.sandbox && !routingOn
|
|
640
|
+
? runWorkspaceMutationForSandbox(access.sandbox, "skillCheckout", mutation)
|
|
641
|
+
: mutation(),
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
});
|
|
558
645
|
const attemptToolDefinitions = [
|
|
646
|
+
...skillTools,
|
|
559
647
|
createListModelsAttemptToolDefinition({
|
|
560
648
|
currentModelId: turnExecutionPolicy.productModelId,
|
|
561
649
|
load: async () => {
|
|
@@ -881,7 +969,11 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
|
|
|
881
969
|
connectorActionPolicy,
|
|
882
970
|
generateSessionTitleInParallel: titleToolPlan.generateTitleInParallel,
|
|
883
971
|
postToolPreparationStartedAt,
|
|
884
|
-
preparationIndependentToolNames:
|
|
972
|
+
preparationIndependentToolNames: [
|
|
973
|
+
...titleToolPlan.preparationIndependentToolNames,
|
|
974
|
+
"skill_read",
|
|
975
|
+
],
|
|
976
|
+
skillCatalog,
|
|
885
977
|
};
|
|
886
978
|
}
|
|
887
979
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
claimAutomationRun,
|
|
18
18
|
settleAutomationRun,
|
|
19
19
|
} from "@opengeni/db";
|
|
20
|
+
import { SessionSkills } from "@opengeni/contracts";
|
|
20
21
|
import { agentRunAdmissionDenial } from "./agent-run-admission";
|
|
21
22
|
import type {
|
|
22
23
|
ControlActivityServices,
|
|
@@ -189,7 +190,8 @@ export function createAutomationActivities(
|
|
|
189
190
|
workspaceId: input.workspaceId,
|
|
190
191
|
initialMessage: accepted.initialMessage,
|
|
191
192
|
resources: template.resources,
|
|
192
|
-
skills: template.skills,
|
|
193
|
+
skills: SessionSkills.parse(template.skills),
|
|
194
|
+
bundledSkillIds: template.bundledSkillIds,
|
|
193
195
|
tools: template.tools,
|
|
194
196
|
toolPolicy: { mode: "explicit", inheritedFromSessionId: null },
|
|
195
197
|
model,
|
|
@@ -362,7 +362,12 @@ async function compactContextRemoteV2(
|
|
|
362
362
|
rewrittenToolOutputs = retry.rewrittenToolOutputs;
|
|
363
363
|
compactionItem = await options.requestRemoteCompactionV2(settings, retry.input);
|
|
364
364
|
}
|
|
365
|
-
const
|
|
365
|
+
const retainedTokens = await retentionTokenCounts(canonicalItems, projectForWire);
|
|
366
|
+
const replacementHistory = buildRemoteV2ReplacementHistory(
|
|
367
|
+
canonicalItems,
|
|
368
|
+
compactionItem,
|
|
369
|
+
(item) => retainedTokens.get(item) ?? estimateTokens([item]),
|
|
370
|
+
);
|
|
366
371
|
const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
|
|
367
372
|
const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
|
|
368
373
|
const tailItem = replacementHistory.at(-1);
|
|
@@ -433,7 +438,12 @@ async function compactContextPortable(
|
|
|
433
438
|
const estimatedTokensBefore = estimateTokens(items);
|
|
434
439
|
const summarized = await summarizeWithCodexOverflowTrimming(summarize, settings, items);
|
|
435
440
|
const summaryBody = summarized.summaryBody;
|
|
436
|
-
const
|
|
441
|
+
const retainedTokens = await retentionTokenCounts(canonicalItems, projectForWire);
|
|
442
|
+
const replacementHistory = buildCompactionReplacementHistory(
|
|
443
|
+
canonicalItems,
|
|
444
|
+
summaryBody,
|
|
445
|
+
(item) => retainedTokens.get(item) ?? estimateTokens([item]),
|
|
446
|
+
);
|
|
437
447
|
const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
|
|
438
448
|
const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
|
|
439
449
|
const previousReplacementFingerprint = latestCompactionReplacementFingerprint(canonicalItems);
|
|
@@ -579,3 +589,17 @@ export function isExactContextLengthExceeded(
|
|
|
579
589
|
isExactContextLengthExceeded(record.diagnostics, seen)
|
|
580
590
|
);
|
|
581
591
|
}
|
|
592
|
+
|
|
593
|
+
/** Charge retained uploads by their projected image context, not reference text. */
|
|
594
|
+
async function retentionTokenCounts(
|
|
595
|
+
items: CompactionItem[],
|
|
596
|
+
project: (items: CompactionItem[]) => Promise<CompactionItem[]>,
|
|
597
|
+
): Promise<Map<CompactionItem, number>> {
|
|
598
|
+
const counts = new Map<CompactionItem, number>();
|
|
599
|
+
for (const item of items) {
|
|
600
|
+
if (item.role === "user" || item.role === "developer") {
|
|
601
|
+
counts.set(item, estimateTokens(await project([item])));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return counts;
|
|
605
|
+
}
|
|
@@ -331,17 +331,50 @@ export async function reconcileAutomaticSessionTitleFanout(
|
|
|
331
331
|
return { claimed: rows.length, delivered, failed };
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
export type WorkflowWakeReconciliationResult = {
|
|
335
|
+
claimed: number;
|
|
336
|
+
/** Transport acceptance is not durable admission. */
|
|
337
|
+
signaled: number;
|
|
338
|
+
/** Revisions acknowledged by the durable admission guard. */
|
|
339
|
+
delivered: number;
|
|
340
|
+
pendingAdmission: number;
|
|
341
|
+
/** Legacy/embedding signalers may not return an acknowledgment receipt. */
|
|
342
|
+
unconfirmed: number;
|
|
343
|
+
failed: number;
|
|
344
|
+
pendingAdmissionBlockers: Partial<
|
|
345
|
+
Record<
|
|
346
|
+
Extract<
|
|
347
|
+
import("@opengeni/db").SessionWorkflowWakeDeliveryResult,
|
|
348
|
+
{ action: "pending_admission" }
|
|
349
|
+
>["blocker"],
|
|
350
|
+
number
|
|
351
|
+
>
|
|
352
|
+
>;
|
|
353
|
+
};
|
|
354
|
+
|
|
334
355
|
export async function reconcilePendingSessionWorkflowWakes(
|
|
335
356
|
svc: NotifyServices,
|
|
336
357
|
limit = 1_000,
|
|
337
358
|
overrides: ReconcileSessionWorkflowWakeOverrides = {},
|
|
338
|
-
): Promise<
|
|
359
|
+
): Promise<WorkflowWakeReconciliationResult> {
|
|
339
360
|
const claimPendingSessionWorkflowWakesFn =
|
|
340
361
|
overrides.claimPendingSessionWorkflowWakes ?? claimPendingSessionWorkflowWakes;
|
|
341
362
|
if (!svc.wakeSessionWorkflow) {
|
|
342
|
-
return {
|
|
363
|
+
return {
|
|
364
|
+
claimed: 0,
|
|
365
|
+
signaled: 0,
|
|
366
|
+
delivered: 0,
|
|
367
|
+
pendingAdmission: 0,
|
|
368
|
+
unconfirmed: 0,
|
|
369
|
+
failed: 0,
|
|
370
|
+
pendingAdmissionBlockers: {},
|
|
371
|
+
};
|
|
343
372
|
}
|
|
344
373
|
const repairs = await claimPendingSessionWorkflowWakesFn(svc.db, limit);
|
|
374
|
+
let signaled = 0;
|
|
375
|
+
let pendingAdmission = 0;
|
|
376
|
+
let unconfirmed = 0;
|
|
377
|
+
const pendingAdmissionBlockers: WorkflowWakeReconciliationResult["pendingAdmissionBlockers"] = {};
|
|
345
378
|
let delivered = 0;
|
|
346
379
|
let failed = 0;
|
|
347
380
|
const queue = [...repairs];
|
|
@@ -349,16 +382,36 @@ export async function reconcilePendingSessionWorkflowWakes(
|
|
|
349
382
|
for (;;) {
|
|
350
383
|
const repair = queue.shift();
|
|
351
384
|
if (!repair) return;
|
|
385
|
+
let signalAccepted = false;
|
|
386
|
+
const onSignalAccepted = () => {
|
|
387
|
+
if (signalAccepted) return;
|
|
388
|
+
signalAccepted = true;
|
|
389
|
+
signaled += 1;
|
|
390
|
+
};
|
|
352
391
|
try {
|
|
353
|
-
await svc.wakeSessionWorkflow!({
|
|
392
|
+
const receipt = await svc.wakeSessionWorkflow!({
|
|
354
393
|
accountId: repair.accountId,
|
|
355
394
|
workspaceId: repair.workspaceId,
|
|
356
395
|
sessionId: repair.sessionId,
|
|
357
396
|
workflowId: repair.temporalWorkflowId,
|
|
358
397
|
wakeRevision: repair.wakeRevision,
|
|
359
398
|
...(repair.interruptionRequested ? { interruptionRequested: true } : {}),
|
|
399
|
+
onSignalAccepted,
|
|
360
400
|
});
|
|
361
|
-
|
|
401
|
+
// Older embeddings do not invoke the optional transport observer.
|
|
402
|
+
// Their successful return still proves a signal call, never an ACK.
|
|
403
|
+
onSignalAccepted();
|
|
404
|
+
if (receipt?.action === "acknowledged") {
|
|
405
|
+
delivered += 1;
|
|
406
|
+
} else if (receipt?.action === "pending_admission") {
|
|
407
|
+
pendingAdmission += 1;
|
|
408
|
+
pendingAdmissionBlockers[receipt.blocker] =
|
|
409
|
+
(pendingAdmissionBlockers[receipt.blocker] ?? 0) + 1;
|
|
410
|
+
} else {
|
|
411
|
+
// A successful legacy signal is not proof of acknowledgment. The
|
|
412
|
+
// committed outbox obligation remains owned by the existing guard.
|
|
413
|
+
unconfirmed += 1;
|
|
414
|
+
}
|
|
362
415
|
} catch (error) {
|
|
363
416
|
failed += 1;
|
|
364
417
|
await markSessionWorkflowWakeFailed(
|
|
@@ -370,7 +423,15 @@ export async function reconcilePendingSessionWorkflowWakes(
|
|
|
370
423
|
}
|
|
371
424
|
});
|
|
372
425
|
await Promise.all(workers);
|
|
373
|
-
return {
|
|
426
|
+
return {
|
|
427
|
+
claimed: repairs.length,
|
|
428
|
+
signaled,
|
|
429
|
+
delivered,
|
|
430
|
+
pendingAdmission,
|
|
431
|
+
unconfirmed,
|
|
432
|
+
failed,
|
|
433
|
+
pendingAdmissionBlockers,
|
|
434
|
+
};
|
|
374
435
|
}
|
|
375
436
|
|
|
376
437
|
function childCompletionPayload(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MODEL_ATTACHMENT_REFS_FIELD,
|
|
3
|
+
MODEL_ATTACHMENT_CATALOG_MARKER,
|
|
3
4
|
FileResourceRef,
|
|
4
5
|
resourceMountPath,
|
|
5
6
|
type FileAsset,
|
|
@@ -134,6 +135,20 @@ async function measureHistoryPreparationPhase<T>(
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
export const MAX_INLINE_MODEL_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
138
|
+
// Raw-byte allowance; base64 and request serialization add memory overhead.
|
|
139
|
+
// This is transport admission, never an instruction to rewrite old messages.
|
|
140
|
+
export const MAX_RETAINED_MODEL_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
141
|
+
|
|
142
|
+
export class RetainedAttachmentTransportLimitError extends Error {
|
|
143
|
+
constructor() {
|
|
144
|
+
super(
|
|
145
|
+
"Active uploaded images exceed the 64 MiB inline transport limit. " +
|
|
146
|
+
"History was preserved and no new image bytes were downloaded. " +
|
|
147
|
+
"Start a smaller conversation or fork before the image-heavy messages.",
|
|
148
|
+
);
|
|
149
|
+
this.name = "RetainedAttachmentTransportLimitError";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
137
152
|
|
|
138
153
|
export type ModelAttachmentContent = {
|
|
139
154
|
kind: "image" | "file";
|
|
@@ -202,13 +217,16 @@ function modelAttachmentDescriptor(
|
|
|
202
217
|
export async function modelAttachmentContentForFiles(
|
|
203
218
|
files: FileAsset[],
|
|
204
219
|
readFileBytes: (file: FileAsset) => Promise<Uint8Array>,
|
|
220
|
+
options: { retainAll?: boolean } = {},
|
|
205
221
|
): Promise<ModelAttachmentContent[]> {
|
|
206
222
|
const selected: Array<{
|
|
207
223
|
file: FileAsset;
|
|
208
224
|
descriptor: Pick<ModelAttachmentContent, "kind" | "contentType">;
|
|
209
225
|
checksum: string;
|
|
210
226
|
}> = [];
|
|
211
|
-
let remainingBytes =
|
|
227
|
+
let remainingBytes = options.retainAll
|
|
228
|
+
? Number.POSITIVE_INFINITY
|
|
229
|
+
: MAX_INLINE_MODEL_ATTACHMENT_BYTES;
|
|
212
230
|
for (const file of files) {
|
|
213
231
|
const descriptor = modelAttachmentDescriptor(file.contentType);
|
|
214
232
|
const checksum = file.sha256?.trim().toLowerCase() ?? "";
|
|
@@ -216,6 +234,7 @@ export async function modelAttachmentContentForFiles(
|
|
|
216
234
|
file.status !== "ready" ||
|
|
217
235
|
!descriptor ||
|
|
218
236
|
descriptor.kind !== "image" ||
|
|
237
|
+
file.sizeBytes > MAX_INLINE_MODEL_ATTACHMENT_BYTES ||
|
|
219
238
|
file.sizeBytes > remainingBytes ||
|
|
220
239
|
!/^[a-f0-9]{64}$/.test(checksum)
|
|
221
240
|
) {
|
|
@@ -301,19 +320,26 @@ function attachmentReceiptText(ref: FileResourceRef, file: FileAsset | undefined
|
|
|
301
320
|
}
|
|
302
321
|
|
|
303
322
|
/**
|
|
304
|
-
* Build one turn-scoped durable-attachment projector.
|
|
305
|
-
*
|
|
306
|
-
*
|
|
323
|
+
* Build one turn-scoped durable-attachment projector. Resolve active refs under
|
|
324
|
+
* current file authority and memoize metadata/bytes for retries and compaction.
|
|
325
|
+
* A turn boundary never changes an authorized attachment's representation.
|
|
307
326
|
*/
|
|
308
327
|
export function createModelHistoryAttachmentProjector(
|
|
309
328
|
policy: ModelAttachmentInputPolicy,
|
|
310
329
|
readFileBytes?: (file: FileAsset) => Promise<Uint8Array>,
|
|
330
|
+
loadAuthorizedFiles?: (fileIds: readonly string[]) => Promise<readonly FileAsset[]>,
|
|
311
331
|
): ModelHistoryAttachmentProjector {
|
|
312
332
|
const contentById = new Map<string, ModelAttachmentContent>();
|
|
313
333
|
const attemptedContentIds = new Set<string>();
|
|
334
|
+
const fileById = new Map<string, FileAsset>();
|
|
335
|
+
const resolvedFileIds = new Set<string>();
|
|
336
|
+
const retainedByteSizes = new Map<string, number>();
|
|
314
337
|
|
|
315
338
|
return async (items, options = {}) => {
|
|
316
|
-
const
|
|
339
|
+
for (const file of options.inlineFiles ?? []) {
|
|
340
|
+
fileById.set(file.id, file);
|
|
341
|
+
resolvedFileIds.add(file.id);
|
|
342
|
+
}
|
|
317
343
|
const refsByIndex = new Map<number, FileResourceRef[]>();
|
|
318
344
|
const orderedFileIds: string[] = [];
|
|
319
345
|
const seenFileIds = new Set<string>();
|
|
@@ -322,27 +348,76 @@ export function createModelHistoryAttachmentProjector(
|
|
|
322
348
|
if (refs.length === 0) continue;
|
|
323
349
|
refsByIndex.set(index, refs);
|
|
324
350
|
for (const ref of refs) {
|
|
325
|
-
if (seenFileIds.has(ref.fileId))
|
|
351
|
+
if (items[index]![MODEL_ATTACHMENT_CATALOG_MARKER] === true || seenFileIds.has(ref.fileId))
|
|
352
|
+
continue;
|
|
326
353
|
seenFileIds.add(ref.fileId);
|
|
327
354
|
orderedFileIds.push(ref.fileId);
|
|
328
355
|
}
|
|
329
356
|
}
|
|
330
357
|
if (refsByIndex.size === 0) return items;
|
|
358
|
+
const unresolved = orderedFileIds.filter((id) => !resolvedFileIds.has(id));
|
|
359
|
+
if (loadAuthorizedFiles && unresolved.length > 0) {
|
|
360
|
+
const files = await loadAuthorizedFiles(unresolved);
|
|
361
|
+
for (const file of files) fileById.set(file.id, file);
|
|
362
|
+
for (const id of unresolved) resolvedFileIds.add(id);
|
|
363
|
+
}
|
|
331
364
|
|
|
332
365
|
if (readFileBytes) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
366
|
+
const admitted = new Map(retainedByteSizes);
|
|
367
|
+
let requestImageBytes = 0;
|
|
368
|
+
for (const [index, refs] of refsByIndex) {
|
|
369
|
+
if (items[index]![MODEL_ATTACHMENT_CATALOG_MARKER] === true) continue;
|
|
370
|
+
for (const ref of refs) {
|
|
371
|
+
const file = fileById.get(ref.fileId);
|
|
372
|
+
if (
|
|
373
|
+
!file ||
|
|
374
|
+
!policy.supportsImageInput ||
|
|
375
|
+
file.status !== "ready" ||
|
|
376
|
+
file.sizeBytes > MAX_INLINE_MODEL_ATTACHMENT_BYTES ||
|
|
377
|
+
modelAttachmentDescriptor(file.contentType)?.kind !== "image" ||
|
|
378
|
+
!/^[a-f0-9]{64}$/i.test(file.sha256?.trim() ?? "")
|
|
379
|
+
)
|
|
380
|
+
continue;
|
|
381
|
+
// Repeated references share cached bytes but repeat in the wire body.
|
|
382
|
+
requestImageBytes += file.sizeBytes;
|
|
383
|
+
admitted.set(file.id, file.sizeBytes);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const turnImageBytes = [...admitted.values()].reduce((sum, bytes) => sum + bytes, 0);
|
|
387
|
+
if (
|
|
388
|
+
requestImageBytes > MAX_RETAINED_MODEL_ATTACHMENT_BYTES ||
|
|
389
|
+
turnImageBytes > MAX_RETAINED_MODEL_ATTACHMENT_BYTES
|
|
390
|
+
) {
|
|
391
|
+
throw new RetainedAttachmentTransportLimitError();
|
|
392
|
+
}
|
|
393
|
+
for (const [id, bytes] of admitted) retainedByteSizes.set(id, bytes);
|
|
394
|
+
// Every active attachment uses the same authorized projection. Metadata
|
|
395
|
+
// and bytes are resolved once per turn, including compaction and retries.
|
|
336
396
|
const readable = [...orderedFileIds]
|
|
337
397
|
.reverse()
|
|
338
|
-
.map((id) =>
|
|
398
|
+
.map((id) => fileById.get(id))
|
|
339
399
|
.filter((file): file is FileAsset => {
|
|
340
400
|
if (!file || attemptedContentIds.has(file.id)) return false;
|
|
341
401
|
const descriptor = modelAttachmentDescriptor(file.contentType);
|
|
342
402
|
return Boolean(descriptor && descriptor.kind === "image" && policy.supportsImageInput);
|
|
343
403
|
});
|
|
344
404
|
for (const file of readable) attemptedContentIds.add(file.id);
|
|
345
|
-
const content = await modelAttachmentContentForFiles(readable, readFileBytes
|
|
405
|
+
const content = await modelAttachmentContentForFiles(readable, readFileBytes, {
|
|
406
|
+
retainAll: true,
|
|
407
|
+
});
|
|
408
|
+
const loadedIds = new Set(content.map((entry) => entry.fileId));
|
|
409
|
+
for (const file of readable) {
|
|
410
|
+
if (
|
|
411
|
+
file.status === "ready" &&
|
|
412
|
+
file.sizeBytes <= MAX_INLINE_MODEL_ATTACHMENT_BYTES &&
|
|
413
|
+
/^[a-f0-9]{64}$/i.test(file.sha256 ?? "") &&
|
|
414
|
+
!loadedIds.has(file.id)
|
|
415
|
+
) {
|
|
416
|
+
// Do not let a retry silently proceed with pixels removed.
|
|
417
|
+
for (const candidate of readable) attemptedContentIds.delete(candidate.id);
|
|
418
|
+
throw new Error(`Retained attachment bytes unavailable or invalid: ${file.id}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
346
421
|
for (const attachment of content) contentById.set(attachment.fileId, attachment);
|
|
347
422
|
}
|
|
348
423
|
|
|
@@ -353,7 +428,8 @@ export function createModelHistoryAttachmentProjector(
|
|
|
353
428
|
? [...original.content]
|
|
354
429
|
: [{ type: "input_text", text: String(original.content ?? "") }];
|
|
355
430
|
const attachmentParts = refs.flatMap((ref) => {
|
|
356
|
-
const currentFile =
|
|
431
|
+
const currentFile =
|
|
432
|
+
original[MODEL_ATTACHMENT_CATALOG_MARKER] === true ? undefined : fileById.get(ref.fileId);
|
|
357
433
|
const attachment = currentFile ? contentById.get(ref.fileId) : undefined;
|
|
358
434
|
const receipt = {
|
|
359
435
|
type: "input_text",
|