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.
- package/dist/modules/runner.d.ts +2 -44
- package/dist/modules/runner.js +2 -604
- package/dist/providers/base.d.ts +1 -45
- package/dist/providers/base.js +0 -67
- package/dist/providers/openai.d.ts +3 -27
- package/dist/providers/openai.js +3 -175
- package/dist/types.d.ts +1 -208
- package/dist/types.js +1 -82
- package/package.json +1 -1
- package/src/modules/runner.ts +3 -797
- package/src/providers/base.ts +1 -86
- package/src/providers/openai.ts +4 -226
- package/src/types.ts +1 -319
package/src/modules/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Runner - Execute Cognitive Modules
|
|
3
|
-
* v2.
|
|
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
|
-
}
|