@memberjunction/server 5.34.1 → 5.35.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/generated/generated.d.ts +21 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +102 -2
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +7 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +28 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +111 -58
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/artifact-routing.d.ts +39 -0
- package/dist/resolvers/artifact-routing.d.ts.map +1 -0
- package/dist/resolvers/artifact-routing.js +40 -0
- package/dist/resolvers/artifact-routing.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +68 -68
- package/src/generated/generated.ts +75 -2
- package/src/generic/ResolverBase.ts +13 -4
- package/src/generic/RunViewResolver.ts +28 -0
- package/src/resolvers/RunAIAgentResolver.ts +119 -61
- package/src/resolvers/__tests__/artifact-routing.test.ts +88 -0
- package/src/resolvers/artifact-routing.ts +80 -0
- package/src/types.ts +6 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine, Subscription, Root, ResolverFilterData, ID, Int } from 'type-graphql';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
4
|
-
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJConversationDetailArtifactEntity, MJArtifactVersionEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJConversationDetailArtifactEntity, MJArtifactVersionEntity, MJAIAgentRequestEntity, ArtifactMetadataEngine } from '@memberjunction/core-entities';
|
|
5
|
+
import { RouteArtifact } from './artifact-routing.js';
|
|
5
6
|
import { AgentRunner, ArtifactToolManager } from '@memberjunction/ai-agents';
|
|
6
7
|
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
|
|
7
8
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
@@ -14,6 +15,15 @@ import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
|
|
|
14
15
|
import { GetAttachmentService } from '@memberjunction/aiengine';
|
|
15
16
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Absolute server-side cap for inline content blocks emitted to the LLM, in
|
|
20
|
+
* bytes. Defense in depth: even if an Artifact Type is configured for Inline
|
|
21
|
+
* delivery, anything past this size falls back to the tool dispatch path with
|
|
22
|
+
* a visible annotation on the manifest. Single source of truth — replaces the
|
|
23
|
+
* pre-existing per-call MAX_INLINE_ARTIFACT_CHARS / maxInlineChars constants.
|
|
24
|
+
*/
|
|
25
|
+
const INLINE_SIZE_CAP = 100 * 1024;
|
|
26
|
+
|
|
17
27
|
@ObjectType()
|
|
18
28
|
export class AIAgentRunResult {
|
|
19
29
|
@Field()
|
|
@@ -1310,84 +1320,132 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1310
1320
|
);
|
|
1311
1321
|
const attachmentDataResults = await Promise.all(attachmentDataPromises);
|
|
1312
1322
|
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
'application/vnd.openxmlformats-officedocument', // .docx, .xlsx, .pptx
|
|
1322
|
-
'application/vnd.ms-excel', // .xls
|
|
1323
|
-
'application/msword', // .doc
|
|
1324
|
-
];
|
|
1325
|
-
|
|
1323
|
+
// Decide inline vs. tools per-attachment via the artifact-type registry
|
|
1324
|
+
// and the pure RouteArtifact() function. See plans/artifact-attachment-unification.md.
|
|
1325
|
+
//
|
|
1326
|
+
// Note on the modality check: this resolver doesn't know which model will
|
|
1327
|
+
// ultimately run, so RouteArtifact's modality predicate is passed as a no-op.
|
|
1328
|
+
// Model-specific modality enforcement is the driver layer's responsibility;
|
|
1329
|
+
// a follow-up PR can thread the active model through here to surface modality
|
|
1330
|
+
// mismatches at the hard-error path described in plan §4.
|
|
1326
1331
|
const validAttachments: AttachmentData[] = [];
|
|
1327
1332
|
|
|
1328
1333
|
for (const result of attachmentDataResults) {
|
|
1329
1334
|
if (!result) continue;
|
|
1335
|
+
// Storage-unified attachments link forward to their artifact version
|
|
1336
|
+
// via ArtifactVersionID; the artifact path handles delivery, so skip
|
|
1337
|
+
// the attachment row here to avoid double-processing.
|
|
1338
|
+
if (result.attachment.ArtifactVersionID) {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1330
1341
|
const mime = result.attachment.MimeType || '';
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1342
|
+
const fileName = result.attachment.FileName ?? '';
|
|
1343
|
+
const ext = fileName.includes('.') ? fileName.split('.').pop() : undefined;
|
|
1344
|
+
const artifactType = ArtifactMetadataEngine.Instance.GetArtifactTypeByMimeType(mime, ext);
|
|
1345
|
+
|
|
1346
|
+
const decision = RouteArtifact({
|
|
1347
|
+
typeDefault: artifactType?.DefaultDeliveryMode ?? 'ToolsOnly',
|
|
1348
|
+
forceToolsOnly: false,
|
|
1349
|
+
mimeType: mime,
|
|
1350
|
+
sizeBytes: result.attachment.FileSizeBytes ?? 0,
|
|
1351
|
+
inlineSizeCap: INLINE_SIZE_CAP,
|
|
1352
|
+
modelSupportsModality: () => true,
|
|
1353
|
+
modelName: '<resolver>',
|
|
1354
|
+
artifactTypeName: artifactType?.Name ?? mime,
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
if (decision.delivery !== 'inline') {
|
|
1358
|
+
// Tools or error: skip inline embedding. The agent reaches the
|
|
1359
|
+
// bytes via artifact tools. Driver layer handles modality enforcement.
|
|
1360
|
+
if (decision.delivery === 'tools' && decision.annotation) {
|
|
1361
|
+
LogStatus(`[RunAIAgentResolver] ${decision.annotation}`);
|
|
1362
|
+
}
|
|
1363
|
+
continue;
|
|
1350
1364
|
}
|
|
1365
|
+
validAttachments.push({
|
|
1366
|
+
type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
|
|
1367
|
+
mimeType: result.attachment.MimeType,
|
|
1368
|
+
fileName: result.attachment.FileName ?? undefined,
|
|
1369
|
+
sizeBytes: result.attachment.FileSizeBytes ?? undefined,
|
|
1370
|
+
width: result.attachment.Width ?? undefined,
|
|
1371
|
+
height: result.attachment.Height ?? undefined,
|
|
1372
|
+
durationSeconds: result.attachment.DurationSeconds ?? undefined,
|
|
1373
|
+
content: result.contentUrl
|
|
1374
|
+
});
|
|
1351
1375
|
}
|
|
1352
1376
|
|
|
1353
|
-
// Get input artifacts for this message
|
|
1354
|
-
// Like regular attachments above, skip file-backed artifacts whose MIME
|
|
1355
|
-
// types are handled by ArtifactToolManager — embedding their multi-MB
|
|
1356
|
-
// base64 content in every conversation replay causes context overflow.
|
|
1377
|
+
// Get input artifacts for this message — same routing logic, plus ForceToolsOnly.
|
|
1357
1378
|
const inputArtifacts = inputArtifactsByDetailId.get(detail.ID) || [];
|
|
1358
1379
|
for (const artifactVersion of inputArtifacts) {
|
|
1359
1380
|
const artifactMime = artifactVersion.MimeType || '';
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
);
|
|
1381
|
+
const fileName = artifactVersion.FileName ?? '';
|
|
1382
|
+
const ext = fileName.includes('.') ? fileName.split('.').pop() : undefined;
|
|
1383
|
+
const artifactType = ArtifactMetadataEngine.Instance.GetArtifactTypeByMimeType(artifactMime, ext);
|
|
1384
|
+
|
|
1385
|
+
const decision = RouteArtifact({
|
|
1386
|
+
typeDefault: artifactType?.DefaultDeliveryMode ?? 'ToolsOnly',
|
|
1387
|
+
forceToolsOnly: artifactVersion.ForceToolsOnly,
|
|
1388
|
+
mimeType: artifactMime,
|
|
1389
|
+
sizeBytes: artifactVersion.ContentSizeBytes ?? 0,
|
|
1390
|
+
inlineSizeCap: INLINE_SIZE_CAP,
|
|
1391
|
+
modelSupportsModality: () => true,
|
|
1392
|
+
modelName: '<resolver>',
|
|
1393
|
+
artifactTypeName: artifactType?.Name ?? artifactMime,
|
|
1394
|
+
});
|
|
1363
1395
|
|
|
1364
1396
|
if (artifactVersion.ContentMode === 'File' && artifactVersion.FileID) {
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
// file input (resolved per-driver in AIPromptRunner).
|
|
1369
|
-
// Do NOT add a placeholder attachment here: 'Document' maps to
|
|
1370
|
-
// 'file_url' content blocks, and drivers attempt base64 decoding
|
|
1371
|
-
// of the placeholder text, causing API errors.
|
|
1372
|
-
} else {
|
|
1373
|
-
// Non-artifact-tool file types: embed normally
|
|
1374
|
-
const fileContent = await this.downloadArtifactFileContent(artifactVersion, contextUser, provider);
|
|
1375
|
-
if (fileContent) {
|
|
1376
|
-
validAttachments.push({
|
|
1377
|
-
type: ConversationUtility.GetAttachmentTypeFromMime(artifactMime),
|
|
1378
|
-
mimeType: artifactMime || 'application/octet-stream',
|
|
1379
|
-
fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
|
|
1380
|
-
sizeBytes: artifactVersion.ContentSizeBytes || undefined,
|
|
1381
|
-
content: fileContent
|
|
1382
|
-
});
|
|
1397
|
+
if (decision.delivery !== 'inline') {
|
|
1398
|
+
if (decision.delivery === 'tools' && decision.annotation) {
|
|
1399
|
+
LogStatus(`[RunAIAgentResolver] ${decision.annotation}`);
|
|
1383
1400
|
}
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const fileContent = await this.downloadArtifactFileContent(artifactVersion, contextUser, provider);
|
|
1404
|
+
if (fileContent) {
|
|
1405
|
+
validAttachments.push({
|
|
1406
|
+
type: ConversationUtility.GetAttachmentTypeFromMime(artifactMime),
|
|
1407
|
+
mimeType: artifactMime || 'application/octet-stream',
|
|
1408
|
+
fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
|
|
1409
|
+
sizeBytes: artifactVersion.ContentSizeBytes || undefined,
|
|
1410
|
+
content: fileContent
|
|
1411
|
+
});
|
|
1384
1412
|
}
|
|
1385
1413
|
} else if (artifactVersion.Content) {
|
|
1386
|
-
// Text artifact
|
|
1387
|
-
//
|
|
1414
|
+
// Text-mode artifact (ContentMode = 'Text'). Honor the
|
|
1415
|
+
// routing decision the same way as for file-mode.
|
|
1416
|
+
if (decision.delivery !== 'inline') {
|
|
1417
|
+
if (decision.delivery === 'tools' && decision.annotation) {
|
|
1418
|
+
LogStatus(`[RunAIAgentResolver] ${decision.annotation}`);
|
|
1419
|
+
}
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1388
1423
|
const textContent = artifactVersion.Content;
|
|
1389
|
-
const MAX_INLINE_ARTIFACT_CHARS = 10_000;
|
|
1390
1424
|
|
|
1425
|
+
// Media artifacts (image / audio / video) stored inline by
|
|
1426
|
+
// the server hook arrive here as `Text` mode with a base64
|
|
1427
|
+
// data URL in Content. We must route them as their native
|
|
1428
|
+
// modality, not as text — otherwise the LLM sees the raw
|
|
1429
|
+
// base64 as text content, can't process it, and either
|
|
1430
|
+
// hallucinates or admits confusion. Use the artifact's
|
|
1431
|
+
// declared MIME (not the wrapper string) so
|
|
1432
|
+
// ConversationUtility builds the right content block type.
|
|
1433
|
+
const mediaModality = artifactMime.startsWith('image/')
|
|
1434
|
+
|| artifactMime.startsWith('audio/')
|
|
1435
|
+
|| artifactMime.startsWith('video/');
|
|
1436
|
+
|
|
1437
|
+
if (mediaModality && textContent.startsWith('data:')) {
|
|
1438
|
+
validAttachments.push({
|
|
1439
|
+
type: ConversationUtility.GetAttachmentTypeFromMime(artifactMime),
|
|
1440
|
+
mimeType: artifactMime,
|
|
1441
|
+
fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
|
|
1442
|
+
sizeBytes: artifactVersion.ContentSizeBytes || undefined,
|
|
1443
|
+
content: textContent,
|
|
1444
|
+
});
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const MAX_INLINE_ARTIFACT_CHARS = 10_000;
|
|
1391
1449
|
if (textContent.length > MAX_INLINE_ARTIFACT_CHARS) {
|
|
1392
1450
|
const preview = ArtifactToolManager.BuildInlinePreview(textContent, 5);
|
|
1393
1451
|
validAttachments.push({
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { RouteArtifact, type ArtifactRoutingInput } from '../artifact-routing';
|
|
3
|
+
|
|
4
|
+
const baseInput = (overrides: Partial<ArtifactRoutingInput> = {}): ArtifactRoutingInput => ({
|
|
5
|
+
typeDefault: 'Inline',
|
|
6
|
+
forceToolsOnly: false,
|
|
7
|
+
mimeType: 'image/png',
|
|
8
|
+
sizeBytes: 5_000,
|
|
9
|
+
inlineSizeCap: 100 * 1024,
|
|
10
|
+
modelSupportsModality: () => true,
|
|
11
|
+
modelName: 'TestModel',
|
|
12
|
+
artifactTypeName: 'Image',
|
|
13
|
+
...overrides,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('RouteArtifact', () => {
|
|
17
|
+
it('routes Inline default + modality supported + under cap to inline', () => {
|
|
18
|
+
const result = RouteArtifact(baseInput());
|
|
19
|
+
expect(result).toEqual({ delivery: 'inline' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('routes ToolsOnly type default to tools', () => {
|
|
23
|
+
const result = RouteArtifact(baseInput({ typeDefault: 'ToolsOnly' }));
|
|
24
|
+
expect(result).toEqual({ delivery: 'tools' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('routes ForceToolsOnly per-instance override to tools regardless of type default', () => {
|
|
28
|
+
const result = RouteArtifact(baseInput({ forceToolsOnly: true }));
|
|
29
|
+
expect(result).toEqual({ delivery: 'tools' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns an error when the model lacks modality support for an Inline type', () => {
|
|
33
|
+
const result = RouteArtifact(baseInput({
|
|
34
|
+
modelSupportsModality: () => false,
|
|
35
|
+
}));
|
|
36
|
+
expect(result.delivery).toBe('error');
|
|
37
|
+
if (result.delivery !== 'error') return;
|
|
38
|
+
expect(result.message).toContain('Image');
|
|
39
|
+
expect(result.message).toContain('TestModel');
|
|
40
|
+
expect(result.message).toContain('image/png');
|
|
41
|
+
// The error message lists all three remediation paths.
|
|
42
|
+
expect(result.message).toMatch(/ToolsOnly/);
|
|
43
|
+
expect(result.message).toMatch(/ForceToolsOnly/);
|
|
44
|
+
expect(result.message).toMatch(/switch to a model/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('falls back to tools with annotation when size exceeds the cap', () => {
|
|
48
|
+
const result = RouteArtifact(baseInput({
|
|
49
|
+
sizeBytes: 200 * 1024,
|
|
50
|
+
inlineSizeCap: 100 * 1024,
|
|
51
|
+
}));
|
|
52
|
+
expect(result.delivery).toBe('tools');
|
|
53
|
+
if (result.delivery !== 'tools') return;
|
|
54
|
+
expect(result.annotation).toBeDefined();
|
|
55
|
+
expect(result.annotation).toMatch(/exceeds the inline cap/);
|
|
56
|
+
expect(result.annotation).toContain('204800');
|
|
57
|
+
expect(result.annotation).toContain('102400');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('checks ToolsOnly before modality (modality check is irrelevant when type is ToolsOnly)', () => {
|
|
61
|
+
const modelSupportsModality = vi.fn(() => false);
|
|
62
|
+
const result = RouteArtifact(baseInput({
|
|
63
|
+
typeDefault: 'ToolsOnly',
|
|
64
|
+
modelSupportsModality,
|
|
65
|
+
}));
|
|
66
|
+
expect(result).toEqual({ delivery: 'tools' });
|
|
67
|
+
expect(modelSupportsModality).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('checks modality before size (modality error wins over size fallback)', () => {
|
|
71
|
+
const result = RouteArtifact(baseInput({
|
|
72
|
+
modelSupportsModality: () => false,
|
|
73
|
+
sizeBytes: 200 * 1024,
|
|
74
|
+
}));
|
|
75
|
+
expect(result.delivery).toBe('error');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('ForceToolsOnly bypasses both modality and size checks', () => {
|
|
79
|
+
const modelSupportsModality = vi.fn(() => false);
|
|
80
|
+
const result = RouteArtifact(baseInput({
|
|
81
|
+
forceToolsOnly: true,
|
|
82
|
+
modelSupportsModality,
|
|
83
|
+
sizeBytes: 200 * 1024,
|
|
84
|
+
}));
|
|
85
|
+
expect(result).toEqual({ delivery: 'tools' });
|
|
86
|
+
expect(modelSupportsModality).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure routing function: decides whether an artifact reaches the LLM via an
|
|
3
|
+
* inline content block (image_url, audio_url, file_url) or via the artifact
|
|
4
|
+
* tool dispatch path. Has no entity-type or framework dependency — operates
|
|
5
|
+
* on plain inputs and returns a discriminated result. Lives next to the
|
|
6
|
+
* resolver but is independently testable.
|
|
7
|
+
*
|
|
8
|
+
* See plans/artifact-attachment-unification.md §4 for the contract.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ArtifactDeliveryMode = 'Inline' | 'ToolsOnly';
|
|
12
|
+
|
|
13
|
+
export interface ArtifactRoutingInput {
|
|
14
|
+
/** The Artifact Type's DefaultDeliveryMode. */
|
|
15
|
+
typeDefault: ArtifactDeliveryMode;
|
|
16
|
+
/** Per-instance opt-out — `true` forces tools regardless of typeDefault. */
|
|
17
|
+
forceToolsOnly: boolean;
|
|
18
|
+
/** MIME type of the artifact content (e.g. 'image/png'). */
|
|
19
|
+
mimeType: string;
|
|
20
|
+
/** Size of the content in bytes. */
|
|
21
|
+
sizeBytes: number;
|
|
22
|
+
/** Maximum inline size in bytes; over this, even Inline-default artifacts go to tools. */
|
|
23
|
+
inlineSizeCap: number;
|
|
24
|
+
/** Predicate: does the active model driver support the given MIME modality inline? */
|
|
25
|
+
modelSupportsModality: (mimeType: string) => boolean;
|
|
26
|
+
/** Model name used in error messages — never used to make decisions. */
|
|
27
|
+
modelName: string;
|
|
28
|
+
/** Artifact type name used in error messages. */
|
|
29
|
+
artifactTypeName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ArtifactRoutingDecision =
|
|
33
|
+
| { delivery: 'inline' }
|
|
34
|
+
| { delivery: 'tools'; annotation?: string }
|
|
35
|
+
| { delivery: 'error'; message: string };
|
|
36
|
+
|
|
37
|
+
export function RouteArtifact(input: ArtifactRoutingInput): ArtifactRoutingDecision {
|
|
38
|
+
const {
|
|
39
|
+
typeDefault,
|
|
40
|
+
forceToolsOnly,
|
|
41
|
+
mimeType,
|
|
42
|
+
sizeBytes,
|
|
43
|
+
inlineSizeCap,
|
|
44
|
+
modelSupportsModality,
|
|
45
|
+
modelName,
|
|
46
|
+
artifactTypeName,
|
|
47
|
+
} = input;
|
|
48
|
+
|
|
49
|
+
// Path 1: ToolsOnly default or per-instance opt-out — always tools.
|
|
50
|
+
if (typeDefault === 'ToolsOnly' || forceToolsOnly) {
|
|
51
|
+
return { delivery: 'tools' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Path 2: Inline default + modality mismatch — hard error.
|
|
55
|
+
// The admin / user has paired an Inline-default type with a model that does
|
|
56
|
+
// not support the modality. There is no defensible runtime fix, so surface
|
|
57
|
+
// it with a remediable message rather than silently falling back to tools.
|
|
58
|
+
if (!modelSupportsModality(mimeType)) {
|
|
59
|
+
return {
|
|
60
|
+
delivery: 'error',
|
|
61
|
+
message:
|
|
62
|
+
`Artifact type "${artifactTypeName}" is configured for Inline delivery but model "${modelName}" does not support modality "${mimeType}". ` +
|
|
63
|
+
`Either configure the type as ToolsOnly, set ForceToolsOnly on this instance, or switch to a model that supports this modality.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Path 3: Inline default + over size cap — documented, annotated fallback.
|
|
68
|
+
// Not silent: the manifest entry carries a visible note and the caller is
|
|
69
|
+
// expected to log at WARN. Both the LLM and the operator can see it
|
|
70
|
+
// happened, so this isn't the same as the silent-fallback antipattern.
|
|
71
|
+
if (sizeBytes >= inlineSizeCap) {
|
|
72
|
+
return {
|
|
73
|
+
delivery: 'tools',
|
|
74
|
+
annotation: `Artifact type "${artifactTypeName}" is configured for Inline delivery but content size (${sizeBytes} bytes) exceeds the inline cap (${inlineSizeCap} bytes); delivered via tools instead.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Path 4: Inline, modality supported, under cap — emit inline content block.
|
|
79
|
+
return { delivery: 'inline' };
|
|
80
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -101,6 +101,12 @@ export type RunViewGenericParams = {
|
|
|
101
101
|
resultType?: string;
|
|
102
102
|
userPayload?: UserPayload;
|
|
103
103
|
aggregates?: AggregateExpression[];
|
|
104
|
+
/**
|
|
105
|
+
* When true, the server-side cache layer is bypassed for this view run —
|
|
106
|
+
* neither the pre-check cache lookup nor the post-query cache write
|
|
107
|
+
* happens. Propagated to `RunViewParams.BypassCache`.
|
|
108
|
+
*/
|
|
109
|
+
bypassCache?: boolean;
|
|
104
110
|
};
|
|
105
111
|
|
|
106
112
|
|