cognitive-modules-cli 1.4.0 → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
- * v2.5: Streaming response and multimodal support
3
+ * v2.2: Envelope format with meta/data separation, risk_rule, repair pass
4
4
  */
5
5
 
6
6
  import type {
@@ -16,36 +16,9 @@ import type {
16
16
  EnvelopeMeta,
17
17
  ModuleResultData,
18
18
  RiskLevel,
19
- RiskRule,
20
- // v2.5 types
21
- StreamingChunk,
22
- MetaChunk,
23
- DeltaChunk,
24
- FinalChunk,
25
- ErrorChunk,
26
- ProgressChunk,
27
- StreamingSession,
28
- MediaInput,
29
- ProviderV25,
30
- CognitiveModuleV25,
31
- ModalityType,
32
- RuntimeCapabilities
19
+ RiskRule
33
20
  } from '../types.js';
34
- import {
35
- aggregateRisk,
36
- isV22Envelope,
37
- isProviderV25,
38
- isModuleV25,
39
- moduleSupportsStreaming,
40
- moduleSupportsMultimodal,
41
- getModuleInputModalities,
42
- ErrorCodesV25,
43
- DEFAULT_RUNTIME_CAPABILITIES
44
- } from '../types.js';
45
- import { randomUUID } from 'crypto';
46
- import { readFile } from 'fs/promises';
47
- import { existsSync } from 'fs';
48
- import { extname } from 'path';
21
+ import { aggregateRisk, isV22Envelope } from '../types.js';
49
22
 
50
23
  export interface RunOptions {
51
24
  // Clean input (v2 style)
@@ -520,770 +493,3 @@ function looksLikeCode(str: string): boolean {
520
493
  ];
521
494
  return codeIndicators.some(re => re.test(str));
522
495
  }
523
-
524
- // =============================================================================
525
- // v2.5 Streaming Support
526
- // =============================================================================
527
-
528
- export interface StreamRunOptions extends RunOptions {
529
- /** Callback for each chunk */
530
- onChunk?: (chunk: StreamingChunk) => void;
531
-
532
- /** Callback for progress updates */
533
- onProgress?: (percent: number, message?: string) => void;
534
-
535
- /** Heartbeat interval in milliseconds (default: 15000) */
536
- heartbeatInterval?: number;
537
-
538
- /** Maximum stream duration in milliseconds (default: 300000) */
539
- maxDuration?: number;
540
- }
541
-
542
- /**
543
- * Create a new streaming session
544
- */
545
- function createStreamingSession(moduleName: string): StreamingSession {
546
- return {
547
- session_id: `sess_${randomUUID().slice(0, 12)}`,
548
- module_name: moduleName,
549
- started_at: Date.now(),
550
- chunks_sent: 0,
551
- accumulated_data: {},
552
- accumulated_text: {}
553
- };
554
- }
555
-
556
- /**
557
- * Create meta chunk (initial streaming response)
558
- */
559
- function createMetaChunk(session: StreamingSession, meta: Partial<EnvelopeMeta>): MetaChunk {
560
- return {
561
- ok: true,
562
- streaming: true,
563
- session_id: session.session_id,
564
- meta
565
- };
566
- }
567
-
568
- /**
569
- * Create delta chunk (incremental content)
570
- * Note: Delta chunks don't include session_id per v2.5 spec
571
- */
572
- function createDeltaChunk(
573
- session: StreamingSession,
574
- field: string,
575
- delta: string
576
- ): DeltaChunk {
577
- session.chunks_sent++;
578
- return {
579
- chunk: {
580
- seq: session.chunks_sent,
581
- type: 'delta',
582
- field,
583
- delta
584
- }
585
- };
586
- }
587
-
588
- /**
589
- * Create progress chunk
590
- * Note: Progress chunks don't include session_id per v2.5 spec
591
- */
592
- function createProgressChunk(
593
- _session: StreamingSession,
594
- percent: number,
595
- stage?: string,
596
- message?: string
597
- ): ProgressChunk {
598
- return {
599
- progress: {
600
- percent,
601
- stage,
602
- message
603
- }
604
- };
605
- }
606
-
607
- /**
608
- * Create final chunk (completion signal)
609
- * Note: Final chunks don't include session_id per v2.5 spec
610
- */
611
- function createFinalChunk(
612
- _session: StreamingSession,
613
- meta: EnvelopeMeta,
614
- data: ModuleResultData,
615
- usage?: { input_tokens: number; output_tokens: number; total_tokens: number }
616
- ): FinalChunk {
617
- return {
618
- final: true,
619
- meta,
620
- data,
621
- usage
622
- };
623
- }
624
-
625
- /**
626
- * Create error chunk
627
- */
628
- function createErrorChunk(
629
- session: StreamingSession,
630
- code: string,
631
- message: string,
632
- recoverable: boolean = false,
633
- partialData?: unknown
634
- ): ErrorChunk {
635
- return {
636
- ok: false,
637
- streaming: true,
638
- session_id: session.session_id,
639
- error: {
640
- code,
641
- message,
642
- recoverable
643
- },
644
- partial_data: partialData
645
- };
646
- }
647
-
648
- /**
649
- * Run module with streaming response
650
- *
651
- * @param module - The cognitive module to execute
652
- * @param provider - The LLM provider
653
- * @param options - Run options including streaming callbacks
654
- * @yields Streaming chunks
655
- */
656
- export async function* runModuleStream(
657
- module: CognitiveModule,
658
- provider: Provider,
659
- options: StreamRunOptions = {}
660
- ): AsyncGenerator<StreamingChunk, ModuleResult | undefined, unknown> {
661
- const {
662
- onChunk,
663
- onProgress,
664
- heartbeatInterval = 15000,
665
- maxDuration = 300000,
666
- ...runOptions
667
- } = options;
668
-
669
- // Create streaming session
670
- const session = createStreamingSession(module.name);
671
- const startTime = Date.now();
672
-
673
- // Check if module supports streaming
674
- if (!moduleSupportsStreaming(module)) {
675
- // Fallback to sync execution
676
- const result = await runModule(module, provider, runOptions);
677
-
678
- // Emit as single final chunk
679
- if (result.ok && 'meta' in result) {
680
- const finalChunk = createFinalChunk(
681
- session,
682
- result.meta,
683
- result.data as ModuleResultData
684
- );
685
- yield finalChunk;
686
- onChunk?.(finalChunk);
687
- return result;
688
- }
689
-
690
- return result;
691
- }
692
-
693
- // Check if provider supports streaming
694
- if (!isProviderV25(provider) || !provider.supportsStreaming?.()) {
695
- // Fallback to sync with warning
696
- console.warn('[cognitive] Provider does not support streaming, falling back to sync');
697
- const result = await runModule(module, provider, runOptions);
698
-
699
- if (result.ok && 'meta' in result) {
700
- const finalChunk = createFinalChunk(
701
- session,
702
- result.meta,
703
- result.data as ModuleResultData
704
- );
705
- yield finalChunk;
706
- onChunk?.(finalChunk);
707
- }
708
-
709
- return result;
710
- }
711
-
712
- // Emit initial meta chunk
713
- const metaChunk = createMetaChunk(session, {
714
- confidence: undefined,
715
- risk: 'low',
716
- explain: 'Processing...'
717
- });
718
- yield metaChunk;
719
- onChunk?.(metaChunk);
720
-
721
- // Build prompt and messages (same as sync)
722
- const { input, args, verbose = false, useEnvelope, useV22 } = runOptions;
723
- const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
724
- const isV22Module = module.tier !== undefined || module.formatVersion === 'v2.2';
725
- const shouldUseV22 = useV22 ?? (isV22Module || module.compat?.runtime_auto_wrap === true);
726
- const riskRule: RiskRule = module.metaConfig?.risk_rule ?? 'max_changes_risk';
727
-
728
- const inputData: ModuleInput = input || {};
729
- if (args && !inputData.code && !inputData.query) {
730
- if (looksLikeCode(args)) {
731
- inputData.code = args;
732
- } else {
733
- inputData.query = args;
734
- }
735
- }
736
-
737
- // Extract media from input
738
- const mediaInputs = extractMediaInputs(inputData);
739
-
740
- // Build prompt with media placeholders
741
- const prompt = buildPromptWithMedia(module, inputData, mediaInputs);
742
-
743
- // Build system message
744
- const systemParts = buildSystemMessage(module, shouldUseEnvelope, shouldUseV22);
745
-
746
- const messages: Message[] = [
747
- { role: 'system', content: systemParts.join('\n') },
748
- { role: 'user', content: prompt },
749
- ];
750
-
751
- try {
752
- // Start streaming invocation
753
- const streamResult = await provider.invokeStream!({
754
- messages,
755
- jsonSchema: module.outputSchema,
756
- temperature: 0.3,
757
- stream: true,
758
- images: mediaInputs.images,
759
- audio: mediaInputs.audio,
760
- video: mediaInputs.video
761
- });
762
-
763
- let accumulatedContent = '';
764
- let lastProgressTime = Date.now();
765
-
766
- // Process stream
767
- for await (const chunk of streamResult.stream) {
768
- // Check timeout
769
- if (Date.now() - startTime > maxDuration) {
770
- const errorChunk = createErrorChunk(
771
- session,
772
- ErrorCodesV25.STREAM_TIMEOUT,
773
- `Stream exceeded max duration of ${maxDuration}ms`,
774
- false,
775
- { partial_content: accumulatedContent }
776
- );
777
- yield errorChunk;
778
- onChunk?.(errorChunk);
779
- return undefined;
780
- }
781
-
782
- // Accumulate content
783
- accumulatedContent += chunk;
784
-
785
- // Emit delta chunk
786
- const deltaChunk = createDeltaChunk(session, 'data.rationale', chunk);
787
- yield deltaChunk;
788
- onChunk?.(deltaChunk);
789
-
790
- // Emit progress periodically
791
- const now = Date.now();
792
- if (now - lastProgressTime > 1000) {
793
- const elapsed = now - startTime;
794
- const estimatedPercent = Math.min(90, Math.floor(elapsed / maxDuration * 100));
795
- const progressChunk = createProgressChunk(
796
- session,
797
- estimatedPercent,
798
- 'generating',
799
- 'Generating response...'
800
- );
801
- yield progressChunk;
802
- onProgress?.(estimatedPercent, 'Generating response...');
803
- lastProgressTime = now;
804
- }
805
- }
806
-
807
- // Parse accumulated response
808
- let parsed: unknown;
809
- try {
810
- const jsonMatch = accumulatedContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
811
- const jsonStr = jsonMatch ? jsonMatch[1] : accumulatedContent;
812
- parsed = JSON.parse(jsonStr.trim());
813
- } catch {
814
- // Try to extract partial JSON
815
- const errorChunk = createErrorChunk(
816
- session,
817
- 'E3001',
818
- `Failed to parse JSON response`,
819
- false,
820
- { raw: accumulatedContent }
821
- );
822
- yield errorChunk;
823
- onChunk?.(errorChunk);
824
- return undefined;
825
- }
826
-
827
- // Process parsed response
828
- let result: ModuleResult;
829
- if (shouldUseEnvelope && typeof parsed === 'object' && parsed !== null && 'ok' in parsed) {
830
- const response = parseEnvelopeResponseLocal(parsed as EnvelopeResponse<unknown>, accumulatedContent);
831
-
832
- if (shouldUseV22 && response.ok && !('meta' in response && response.meta)) {
833
- const upgraded = wrapV21ToV22Local(parsed as EnvelopeResponse<unknown>, riskRule);
834
- result = {
835
- ok: true,
836
- meta: upgraded.meta as EnvelopeMeta,
837
- data: (upgraded as { data?: ModuleResultData }).data,
838
- raw: accumulatedContent
839
- } as ModuleResultV22;
840
- } else {
841
- result = response;
842
- }
843
- } else {
844
- result = parseLegacyResponseLocal(parsed, accumulatedContent);
845
-
846
- if (shouldUseV22 && result.ok) {
847
- const data = (result.data ?? {}) as Record<string, unknown>;
848
- result = {
849
- ok: true,
850
- meta: {
851
- confidence: (data.confidence as number) ?? 0.5,
852
- risk: aggregateRisk(data, riskRule),
853
- explain: ((data.rationale as string) ?? '').slice(0, 280) || 'No explanation provided'
854
- },
855
- data: result.data,
856
- raw: accumulatedContent
857
- } as ModuleResultV22;
858
- }
859
- }
860
-
861
- // Emit final chunk
862
- if (result.ok && 'meta' in result) {
863
- const finalChunk = createFinalChunk(
864
- session,
865
- result.meta,
866
- result.data as ModuleResultData,
867
- streamResult.usage ? {
868
- input_tokens: streamResult.usage.promptTokens,
869
- output_tokens: streamResult.usage.completionTokens,
870
- total_tokens: streamResult.usage.totalTokens
871
- } : undefined
872
- );
873
- yield finalChunk;
874
- onChunk?.(finalChunk);
875
- onProgress?.(100, 'Complete');
876
- }
877
-
878
- return result;
879
-
880
- } catch (error) {
881
- const errorChunk = createErrorChunk(
882
- session,
883
- ErrorCodesV25.STREAM_INTERRUPTED,
884
- error instanceof Error ? error.message : 'Stream interrupted',
885
- true
886
- );
887
- yield errorChunk;
888
- onChunk?.(errorChunk);
889
- return undefined;
890
- }
891
- }
892
-
893
- // Local versions of helper functions to avoid circular issues
894
- function parseEnvelopeResponseLocal(response: EnvelopeResponse<unknown>, raw: string): ModuleResult {
895
- if (isV22Envelope(response)) {
896
- if (response.ok) {
897
- return {
898
- ok: true,
899
- meta: response.meta,
900
- data: response.data as ModuleResultData,
901
- raw,
902
- } as ModuleResultV22;
903
- } else {
904
- return {
905
- ok: false,
906
- meta: response.meta,
907
- error: response.error,
908
- partial_data: response.partial_data,
909
- raw,
910
- } as ModuleResultV22;
911
- }
912
- }
913
-
914
- if (response.ok) {
915
- const data = (response.data ?? {}) as ModuleResultData & { confidence?: number };
916
- return {
917
- ok: true,
918
- data: {
919
- ...data,
920
- confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
921
- rationale: typeof data.rationale === 'string' ? data.rationale : '',
922
- behavior_equivalence: data.behavior_equivalence,
923
- },
924
- raw,
925
- } as ModuleResultV21;
926
- } else {
927
- return {
928
- ok: false,
929
- error: response.error,
930
- partial_data: response.partial_data,
931
- raw,
932
- } as ModuleResultV21;
933
- }
934
- }
935
-
936
- function wrapV21ToV22Local(
937
- response: EnvelopeResponse<unknown>,
938
- riskRule: RiskRule = 'max_changes_risk'
939
- ): EnvelopeResponseV22<unknown> {
940
- if (isV22Envelope(response)) {
941
- return response;
942
- }
943
-
944
- if (response.ok) {
945
- const data = (response.data ?? {}) as Record<string, unknown>;
946
- const confidence = (data.confidence as number) ?? 0.5;
947
- const rationale = (data.rationale as string) ?? '';
948
-
949
- return {
950
- ok: true,
951
- meta: {
952
- confidence,
953
- risk: aggregateRisk(data, riskRule),
954
- explain: rationale.slice(0, 280) || 'No explanation provided'
955
- },
956
- data: data as ModuleResultData
957
- };
958
- } else {
959
- const errorMsg = response.error?.message ?? 'Unknown error';
960
- return {
961
- ok: false,
962
- meta: {
963
- confidence: 0,
964
- risk: 'high',
965
- explain: errorMsg.slice(0, 280)
966
- },
967
- error: response.error ?? { code: 'UNKNOWN', message: errorMsg },
968
- partial_data: response.partial_data
969
- };
970
- }
971
- }
972
-
973
- function parseLegacyResponseLocal(output: unknown, raw: string): ModuleResult {
974
- const outputObj = output as Record<string, unknown>;
975
- const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
976
- const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
977
- const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
978
- ? outputObj.behavior_equivalence
979
- : undefined;
980
-
981
- if (outputObj.error && typeof outputObj.error === 'object') {
982
- const errorObj = outputObj.error as Record<string, unknown>;
983
- if (typeof errorObj.code === 'string') {
984
- return {
985
- ok: false,
986
- error: {
987
- code: errorObj.code,
988
- message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
989
- },
990
- raw,
991
- };
992
- }
993
- }
994
-
995
- return {
996
- ok: true,
997
- data: {
998
- ...outputObj,
999
- confidence,
1000
- rationale,
1001
- behavior_equivalence: behaviorEquivalence,
1002
- },
1003
- raw,
1004
- } as ModuleResultV21;
1005
- }
1006
-
1007
- // =============================================================================
1008
- // v2.5 Multimodal Support
1009
- // =============================================================================
1010
-
1011
- interface ExtractedMedia {
1012
- images: MediaInput[];
1013
- audio: MediaInput[];
1014
- video: MediaInput[];
1015
- }
1016
-
1017
- /**
1018
- * Extract media inputs from module input data
1019
- */
1020
- function extractMediaInputs(input: ModuleInput): ExtractedMedia {
1021
- const images: MediaInput[] = [];
1022
- const audio: MediaInput[] = [];
1023
- const video: MediaInput[] = [];
1024
-
1025
- // Check for images array
1026
- if (Array.isArray(input.images)) {
1027
- for (const img of input.images) {
1028
- if (isValidMediaInput(img)) {
1029
- images.push(img);
1030
- }
1031
- }
1032
- }
1033
-
1034
- // Check for audio array
1035
- if (Array.isArray(input.audio)) {
1036
- for (const aud of input.audio) {
1037
- if (isValidMediaInput(aud)) {
1038
- audio.push(aud);
1039
- }
1040
- }
1041
- }
1042
-
1043
- // Check for video array
1044
- if (Array.isArray(input.video)) {
1045
- for (const vid of input.video) {
1046
- if (isValidMediaInput(vid)) {
1047
- video.push(vid);
1048
- }
1049
- }
1050
- }
1051
-
1052
- return { images, audio, video };
1053
- }
1054
-
1055
- /**
1056
- * Validate media input structure
1057
- */
1058
- function isValidMediaInput(input: unknown): input is MediaInput {
1059
- if (typeof input !== 'object' || input === null) return false;
1060
- const obj = input as Record<string, unknown>;
1061
-
1062
- if (obj.type === 'url' && typeof obj.url === 'string') return true;
1063
- if (obj.type === 'base64' && typeof obj.data === 'string' && typeof obj.media_type === 'string') return true;
1064
- if (obj.type === 'file' && typeof obj.path === 'string') return true;
1065
-
1066
- return false;
1067
- }
1068
-
1069
- /**
1070
- * Build prompt with media placeholders
1071
- */
1072
- function buildPromptWithMedia(
1073
- module: CognitiveModule,
1074
- input: ModuleInput,
1075
- media: ExtractedMedia
1076
- ): string {
1077
- let prompt = buildPrompt(module, input);
1078
-
1079
- // Replace $MEDIA_INPUTS placeholder
1080
- if (prompt.includes('$MEDIA_INPUTS')) {
1081
- const mediaSummary = buildMediaSummary(media);
1082
- prompt = prompt.replace(/\$MEDIA_INPUTS/g, mediaSummary);
1083
- }
1084
-
1085
- return prompt;
1086
- }
1087
-
1088
- /**
1089
- * Build summary of media inputs for prompt
1090
- */
1091
- function buildMediaSummary(media: ExtractedMedia): string {
1092
- const parts: string[] = [];
1093
-
1094
- if (media.images.length > 0) {
1095
- parts.push(`[${media.images.length} image(s) attached]`);
1096
- }
1097
- if (media.audio.length > 0) {
1098
- parts.push(`[${media.audio.length} audio file(s) attached]`);
1099
- }
1100
- if (media.video.length > 0) {
1101
- parts.push(`[${media.video.length} video file(s) attached]`);
1102
- }
1103
-
1104
- return parts.length > 0 ? parts.join('\n') : '[No media attached]';
1105
- }
1106
-
1107
- /**
1108
- * Build system message for module execution
1109
- */
1110
- function buildSystemMessage(
1111
- module: CognitiveModule,
1112
- shouldUseEnvelope: boolean,
1113
- shouldUseV22: boolean
1114
- ): string[] {
1115
- const systemParts: string[] = [
1116
- `You are executing the "${module.name}" Cognitive Module.`,
1117
- '',
1118
- `RESPONSIBILITY: ${module.responsibility}`,
1119
- ];
1120
-
1121
- if (module.excludes.length > 0) {
1122
- systemParts.push('', 'YOU MUST NOT:');
1123
- module.excludes.forEach(e => systemParts.push(`- ${e}`));
1124
- }
1125
-
1126
- if (module.constraints) {
1127
- systemParts.push('', 'CONSTRAINTS:');
1128
- if (module.constraints.no_network) systemParts.push('- No network access');
1129
- if (module.constraints.no_side_effects) systemParts.push('- No side effects');
1130
- if (module.constraints.no_file_write) systemParts.push('- No file writes');
1131
- if (module.constraints.no_inventing_data) systemParts.push('- Do not invent data');
1132
- }
1133
-
1134
- if (module.output?.require_behavior_equivalence) {
1135
- systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
1136
- systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
1137
- systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
1138
-
1139
- const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
1140
- systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
1141
- }
1142
-
1143
- // Add multimodal instructions if module supports it
1144
- if (isModuleV25(module) && moduleSupportsMultimodal(module)) {
1145
- const inputModalities = getModuleInputModalities(module);
1146
- systemParts.push('', 'MULTIMODAL INPUT:');
1147
- systemParts.push(`- This module accepts: ${inputModalities.join(', ')}`);
1148
- systemParts.push('- Analyze any attached media carefully');
1149
- systemParts.push('- Reference specific elements from the media in your analysis');
1150
- }
1151
-
1152
- // Add envelope format instructions
1153
- if (shouldUseEnvelope) {
1154
- if (shouldUseV22) {
1155
- systemParts.push('', 'RESPONSE FORMAT (Envelope v2.2):');
1156
- systemParts.push('- Wrap your response in the v2.2 envelope format with separate meta and data');
1157
- systemParts.push('- Success: { "ok": true, "meta": { "confidence": 0.9, "risk": "low", "explain": "short summary" }, "data": { ...payload... } }');
1158
- systemParts.push('- Error: { "ok": false, "meta": { "confidence": 0.0, "risk": "high", "explain": "error summary" }, "error": { "code": "ERROR_CODE", "message": "..." } }');
1159
- systemParts.push('- meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.');
1160
- systemParts.push('- meta.risk must be one of: "none", "low", "medium", "high"');
1161
- } else {
1162
- systemParts.push('', 'RESPONSE FORMAT (Envelope):');
1163
- systemParts.push('- Wrap your response in the envelope format');
1164
- systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
1165
- systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
1166
- systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
1167
- }
1168
- if (module.output?.require_behavior_equivalence) {
1169
- systemParts.push('- Include "behavior_equivalence" (boolean) in data');
1170
- }
1171
- } else {
1172
- systemParts.push('', 'OUTPUT FORMAT:');
1173
- systemParts.push('- Respond with ONLY valid JSON');
1174
- systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
1175
- if (module.output?.require_behavior_equivalence) {
1176
- systemParts.push('- Include "behavior_equivalence" (boolean) field');
1177
- }
1178
- }
1179
-
1180
- return systemParts;
1181
- }
1182
-
1183
- /**
1184
- * Load media file as base64
1185
- */
1186
- export async function loadMediaAsBase64(path: string): Promise<{ data: string; media_type: string } | null> {
1187
- try {
1188
- if (!existsSync(path)) {
1189
- return null;
1190
- }
1191
-
1192
- const buffer = await readFile(path);
1193
- const data = buffer.toString('base64');
1194
- const media_type = getMediaTypeFromExtension(extname(path));
1195
-
1196
- return { data, media_type };
1197
- } catch {
1198
- return null;
1199
- }
1200
- }
1201
-
1202
- /**
1203
- * Get MIME type from file extension
1204
- */
1205
- function getMediaTypeFromExtension(ext: string): string {
1206
- const mimeTypes: Record<string, string> = {
1207
- '.jpg': 'image/jpeg',
1208
- '.jpeg': 'image/jpeg',
1209
- '.png': 'image/png',
1210
- '.gif': 'image/gif',
1211
- '.webp': 'image/webp',
1212
- '.mp3': 'audio/mpeg',
1213
- '.wav': 'audio/wav',
1214
- '.ogg': 'audio/ogg',
1215
- '.webm': 'audio/webm',
1216
- '.mp4': 'video/mp4',
1217
- '.mov': 'video/quicktime',
1218
- '.pdf': 'application/pdf'
1219
- };
1220
-
1221
- return mimeTypes[ext.toLowerCase()] ?? 'application/octet-stream';
1222
- }
1223
-
1224
- /**
1225
- * Validate media input against module constraints
1226
- */
1227
- export function validateMediaInput(
1228
- media: MediaInput,
1229
- module: CognitiveModule,
1230
- maxSizeMb: number = 20
1231
- ): { valid: boolean; error?: string; code?: string } {
1232
- // Check if module supports multimodal
1233
- if (!moduleSupportsMultimodal(module)) {
1234
- return {
1235
- valid: false,
1236
- error: 'Module does not support multimodal input',
1237
- code: ErrorCodesV25.MULTIMODAL_NOT_SUPPORTED
1238
- };
1239
- }
1240
-
1241
- // Validate media type
1242
- if (media.type === 'base64') {
1243
- const mediaType = (media as { media_type: string }).media_type;
1244
- if (!isValidMediaType(mediaType)) {
1245
- return {
1246
- valid: false,
1247
- error: `Unsupported media type: ${mediaType}`,
1248
- code: ErrorCodesV25.UNSUPPORTED_MEDIA_TYPE
1249
- };
1250
- }
1251
- }
1252
-
1253
- // Size validation would require fetching/checking actual data
1254
- // This is a placeholder for the check
1255
-
1256
- return { valid: true };
1257
- }
1258
-
1259
- /**
1260
- * Check if media type is supported
1261
- */
1262
- function isValidMediaType(mediaType: string): boolean {
1263
- const supported = [
1264
- 'image/jpeg', 'image/png', 'image/webp', 'image/gif',
1265
- 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm',
1266
- 'video/mp4', 'video/webm', 'video/quicktime',
1267
- 'application/pdf'
1268
- ];
1269
-
1270
- return supported.includes(mediaType);
1271
- }
1272
-
1273
- /**
1274
- * Get runtime capabilities
1275
- */
1276
- export function getRuntimeCapabilities(): RuntimeCapabilities {
1277
- return { ...DEFAULT_RUNTIME_CAPABILITIES };
1278
- }
1279
-
1280
- /**
1281
- * Check if runtime supports a specific modality
1282
- */
1283
- export function runtimeSupportsModality(
1284
- modality: ModalityType,
1285
- direction: 'input' | 'output' = 'input'
1286
- ): boolean {
1287
- const caps = getRuntimeCapabilities();
1288
- return caps.multimodal[direction].includes(modality);
1289
- }